hitl_tester.test_cases.bms.test_milprf_2590

Test MIL-PRF Compatability
GitHub Issue(s) turnaroundfactor/HITL#327
turnaroundfactor/HITL#342
Description Tests for MIL-PRF-32383/5A (16-December-2021) compatibility.

Used in these test plans:

  • dev_bms ⠀⠀⠀(bms/dev_bms.plan)
  • prod_bms ⠀⠀⠀(bms/prod_bms.plan)
  • milprf_2590 ⠀⠀⠀(bms/milprf_2590.plan)
  • qa_bms ⠀⠀⠀(bms/qa_bms.plan)

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

  • ./hitl_tester.py dev_bms -DFAST_MODE=False -DCELL_CAPACITY_AH=0 -DFLASH_SLEEP=7 -DCELL_VOLTAGE=3.8002
   1"""
   2| Test                 | MIL-PRF Compatability                                        |
   3| :------------------- | :----------------------------------------------------------- |
   4| GitHub Issue(s)      | turnaroundfactor/HITL#327                        </br>\
   5                         turnaroundfactor/HITL#342                             |
   6| Description          | Tests for MIL-PRF-32383/5A (16-December-2021) compatibility. |
   7"""
   8
   9from __future__ import annotations
  10
  11import ctypes
  12import math
  13import statistics
  14import time
  15from types import SimpleNamespace
  16from typing import cast
  17
  18import pytest
  19
  20from hitl_tester.modules.bms.adc_plate import ADCPlate
  21from hitl_tester.modules.bms.bms_hw import BMSHardware
  22from hitl_tester.modules.bms.bms_serial import serial_monitor
  23from hitl_tester.modules.bms.cell import Cell
  24from hitl_tester.modules.bms.event_watcher import SerialWatcher
  25from hitl_tester.modules.bms.plateset import Plateset
  26from hitl_tester.modules.bms.smbus import SMBus
  27from hitl_tester.modules.bms.smbus_types import SMBusReg, BatteryMode, BMSCommands
  28from hitl_tester.modules.bms.test_handler import CSVRecordEvent
  29from hitl_tester.modules.bms_types import DischargeType, ControlStatusRegister, BMSState
  30from hitl_tester.modules.logger import logger
  31
  32FAST_MODE = False
  33"""Shorten test times if True."""
  34
  35CELL_CAPACITY_AH = 0
  36"""Cell capacity combined."""
  37
  38FLASH_SLEEP = 7
  39"""How long to wait for flash operations in seconds."""
  40
  41CELL_VOLTAGE = 3.8002
  42"""Default cell voltage."""
  43
  44_bms = BMSHardware(pytest.flags)  # type: ignore[arg-type]
  45_bms.init()
  46_plateset = Plateset()
  47_adc_plate = ADCPlate()
  48_smbus = SMBus()
  49_serial_watcher = SerialWatcher()
  50
  51
  52@pytest.fixture(scope="function", autouse=True)
  53def reset_test_environment(request):
  54    """
  55    Before each test, reset cell sims / BMS and set appropriate temperatures.
  56    After each test, clean up modified objects.
  57
  58    Fixture arguments are provided in an abnormal way, see below tests for details on how to provide these arguments.
  59    A default value is used if neither soc nor volts is provided.
  60
  61    :param float temperature: the initial temperature in C
  62    :param float soc: the initial state of charge
  63    :param float volts: the initial voltage
  64    """
  65    global CELL_CAPACITY_AH
  66
  67    request.param = getattr(request, "param", {})
  68
  69    # Reset cell sims
  70    starting_temperature = request.param.get("temperature", 23)
  71    if len(_bms.cells) > 0:
  72        logger.write_info_to_report(f"Setting temperature to {starting_temperature}°C")
  73        _plateset.thermistor1 = _plateset.thermistor2 = starting_temperature
  74
  75        logger.write_info_to_report("Powering down cell sims")
  76        for cell in _bms.cells.values():
  77            cell.disengage_safety_protocols = True
  78            cell.volts = 0.0001
  79        time.sleep(5)
  80
  81        for cell in _bms.cells.values():
  82            new_soc = request.param.get("soc") or cell.volts_to_soc(request.param.get("volts")) or 0.50
  83            logger.write_info_to_report(f"Powering up cell sim {cell.id} to {new_soc:%}")
  84            cell.state_of_charge = new_soc
  85            cell.disengage_safety_protocols = False
  86
  87        logger.write_info_to_report("Waiting 10 seconds for BMS...")
  88        time.sleep(10)
  89
  90        if not CELL_CAPACITY_AH and (serial_data := serial_monitor.read()):  # Get capacity from BMS
  91            CELL_CAPACITY_AH = float(serial_data["milliamp_hour_capacity"]) / 1000
  92        logger.write_info_to_report(f"Setting cell capacity to {CELL_CAPACITY_AH} Ah")
  93        for cell in _bms.cells.values():
  94            cell.data.capacity = CELL_CAPACITY_AH
  95
  96    # Clear permanent disables
  97    serial_monitor.read()  # Clear latest serial buffer
  98    serial_data = serial_monitor.read()
  99    for key in serial_data:
 100        if key.startswith(("flags.permanent", "flags.measure_output_fets_disabled")) and serial_data[key]:
 101            # Erase flash
 102            logger.write_warning_to_report("Detected permanent fault.")
 103            logger.write_info_to_report("Erasing flash...")
 104            _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.ERASE_FLASH)
 105            time.sleep(FLASH_SLEEP)  # Wait for erase to complete
 106            data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0]
 107            logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}")
 108
 109            # Enable faults
 110            logger.write_info_to_report("Enabling faults...")
 111            try:
 112                test_fault_enable()
 113            except TimeoutError:
 114                logger.write_error_to_report("Failed to enable faults.")
 115
 116            # Recalibrate
 117            logger.write_info_to_report("Recalibrating...")
 118            try:
 119                TestCalibration().test_calibration()
 120            except AssertionError:
 121                logger.write_error_to_report("Failed to calibrate.")
 122            break
 123
 124    CSVRecordEvent.current_test(request.cls)  # Automatically register any tests defined in this class
 125
 126    yield  # Run test
 127
 128    CSVRecordEvent.failed()  # Record results regardless of failure or success
 129    CSVRecordEvent.clear_tests()
 130
 131
 132def standard_charge(
 133    charge_current: float = 2,
 134    max_time: int = 8 * 3600,
 135    sample_interval: int = 10,
 136    minimum_readings: int = 3,
 137    termination_current: float = 0.100,
 138):
 139    """
 140    Helper function to charge batteries in accordance with 4.3.1 for not greater than three hours.
 141    4.3.1 = 23 ± 5°C (73.4°F) ambient pressure/relative humidity, with 2+ hours between charge and discharge.
 142    """
 143    _bms.voltage = 16.8
 144    _bms.ov_protection = _bms.voltage + 0.050  # 50mV above the charging voltage
 145    _bms.current = charge_current
 146    _bms.termination_current = termination_current  # 100 mA
 147    _bms.max_time = max_time
 148    _bms.sample_interval = sample_interval
 149    _bms.minimum_readings = minimum_readings
 150
 151    # Run the Charge cycle
 152    _plateset.ce_switch = True
 153    _bms.run_li_charge_cycle()
 154    _plateset.ce_switch = False
 155
 156
 157def standard_rest(seconds: float = 2 * 3600, sample_interval: int = 10):
 158    """Helper function to stabilize the batteries for 2+ hours."""
 159    _bms.max_time = seconds
 160    _bms.sample_interval = sample_interval
 161    _bms.run_resting_cycle()
 162
 163
 164def standard_discharge(
 165    discharge_current: float = 2, max_time: int = 8 * 3600, sample_interval: int = 10, discharge_voltage: float = 10
 166):
 167    """Helper function to discharge at 2A until 10V."""
 168    _bms.voltage = discharge_voltage
 169    _bms.uv_protection = _bms.voltage - 0.500  # 500mV below voltage cutoff
 170    _bms.current = discharge_current
 171    _bms.discharge_type = DischargeType.CONSTANT_CURRENT
 172    _bms.max_time = max_time
 173    _bms.sample_interval = sample_interval
 174
 175    # Run the discharge cycle, returning the capacity
 176    capacity = _bms.run_discharge_cycle()
 177    logger.write_info_to_report(f"Discharge complete, capacity was {capacity * 1000.0} mAh")
 178    return capacity
 179
 180
 181def test_fault_enable():
 182    """
 183    | Description          | Enable faults via SMBus.                                               |
 184    | :------------------- | :--------------------------------------------------------------------- |
 185    | GitHub Issue         | turnaroundfactor/HITL#476                                       |
 186    | Instructions         | 1. Enable faults via SMBus                                        </br>\
 187                             2. Raise an over-temp fault                                       </br>\
 188                             3. Clear the over-temp fault                                           |
 189    | Pass / Fail Criteria | Pass if faults can be raised                                           |
 190    | Estimated Duration   | 1 minute                                                               |
 191    """
 192
 193    _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.FAULT_ENABLE)
 194    time.sleep(FLASH_SLEEP)
 195
 196    # Check if overtemp faults can be raised.
 197    timeout_s = 30
 198
 199    # Raise a fault
 200    _plateset.thermistor1 = 65
 201    start = time.perf_counter()
 202    while (serial_data := serial_monitor.read(latest=True)) and not serial_data["flags.fault_overtemp_discharge"]:
 203        if time.perf_counter() - start > timeout_s:
 204            message = f"Over-temperature fault was not raised after {timeout_s} seconds."
 205            logger.write_failure_to_html_report(message)
 206            raise TimeoutError(message)
 207    logger.write_result_to_html_report("Fault successfully raised.")
 208
 209    # Clear the fault
 210    _plateset.thermistor1 = 45
 211    start = time.perf_counter()
 212    while (serial_data := serial_monitor.read(latest=True)) and serial_data["flags.fault_overtemp_discharge"]:
 213        if time.perf_counter() - start > timeout_s:
 214            message = f"Over-temperature fault was not cleared after {timeout_s} seconds."
 215            logger.write_failure_to_html_report(message)
 216            raise TimeoutError(message)
 217    logger.write_result_to_html_report("Fault successfully cleared.")
 218
 219
 220class TestCalibration:
 221    """Calibrate the BMS if needed."""
 222
 223    average = 0
 224    readings = 0
 225
 226    def bms_current(self):
 227        """Measure serial current and calculate an average."""
 228        assert (serial_date := serial_monitor.read()), "Could not read serial."
 229        new_reading = serial_date["mamps"]
 230        self.average = (new_reading + self.readings * self.average) / (self.readings + 1)
 231        self.readings += 1
 232        logger.write_info_to_report(f"BMS Serial Current (mA): {new_reading:.3f}")  # Output current on every sample
 233
 234    def test_calibration(self):
 235        """
 236        | Description          | Test calibration retention                                             |
 237        | :------------------- | :--------------------------------------------------------------------- |
 238        | GitHub Issue         | turnaroundfactor/HITL#413                                       |
 239        | Instructions         | 1. Calibrate the BMS                                              </br>\
 240                                 2. Confirm BMS is calibrated                                           |
 241        | Pass / Fail Criteria | Pass if calibrated                                                     |
 242        | Estimated Duration   | 1 minute                                                               |
 243        """
 244        acceptable_error_ma = 5
 245        scan_count = 6  # How many measurements to take for an average.
 246
 247        self.readings = 0  # Reset average
 248        for _ in range(scan_count):  # Measure current over some period
 249            self.bms_current()
 250            time.sleep(5)
 251        logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA")
 252        offset = int(round(self.average, 0))
 253        logger.write_info_to_report(f"Setting offset current to {offset:.3f} mA")
 254        data = (ctypes.c_uint8(offset).value << 8) | BMSCommands.CALIBRATE
 255        _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, data)
 256        time.sleep(FLASH_SLEEP)
 257        data = cast(int, _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0])
 258        logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}")
 259
 260        # Confirm current is in acceptable range
 261        self.readings = 0  # Reset average
 262        for _ in range(scan_count):
 263            self.bms_current()
 264            time.sleep(5)
 265        logger.write_result_to_html_report(f"Average rest current (calibrated): {self.average:.3f} mA")
 266        assert (
 267            acceptable_error_ma > self.average > -acceptable_error_ma
 268        ), f"{self.average:.3f} mA outside limit of ±{acceptable_error_ma:.3f} mA"
 269
 270
 271def test_charge_enable_on():
 272    """
 273    | Description          | Confirm charging above 400mA works when CE is active             |
 274    | :------------------- | :--------------------------------------------------------------- |
 275    | GitHub Issue         | turnaroundfactor/HITL#342                                 |
 276    | MIL-PRF Section      | 3.5.6.2 (Charge Enable)                                          |
 277    | MIL-PRF Requirements | The charge enable terminal shall comply with the following: </br>\
 278                              ⠀⠀a. Maximum charge without enable: 400 mA                 </br>\
 279                              ⠀⠀b. Equivalent resistor: 235 Ω                            </br>\
 280                              ⠀⠀c. Equivalent diode VF: 1.3 V                            </br>\
 281                              ⠀⠀d. Approximate activation current: 7 mA                       |
 282    | Instructions         | 1. Activate CE                                               </br>\
 283                             2. Charge at 2A                                                  |
 284    | Pass / Fail Criteria | Pass if current is more than 400mA                               |
 285    | Estimated Duration   | 1 minute                                                         |
 286    | Note                 | This test can fail if the battery is sufficiently charged.       |
 287    """
 288    passing_current = 0.400
 289
 290    # Enable charging and timer
 291    _plateset.ce_switch = True
 292    _bms.charger.set_profile(volts=16.8, amps=2)
 293    _bms.charger.enable()
 294    _bms.timer.reset()  # Keep track of runtime
 295
 296    # Charge until current is more than passing_current or timeout
 297    timeout_seconds = 10
 298    while (latest_current := _bms.charger.amps) < passing_current and _bms.timer.elapsed_time <= timeout_seconds:
 299        logger.write_info_to_report(
 300            f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Voltage: {_bms.dmm.volts:.3f}, "
 301            f"Current: {latest_current:.3f}"
 302        )
 303        time.sleep(1)
 304
 305    # Charging is complete, turn off the charger
 306    _bms.charger.disable()
 307    _plateset.ce_switch = False
 308
 309    # Check results
 310    logger.write_result_to_html_report(f"Current: {latest_current:.3f} A ≥ {passing_current:.3f} A")
 311    if latest_current < passing_current:
 312        pytest.fail(f"Current of {latest_current:.3f} A does not exceed {passing_current:.3f} A.")
 313
 314
 315def test_charge_enable_off():
 316    """
 317    | Description          | Confirm charging above 400mA doesn't work when CE is inactive          |
 318    | :------------------- | :--------------------------------------------------------------------- |
 319    | GitHub Issue         | turnaroundfactor/HITL#342                                       |
 320    | MIL-PRF Section      | 3.5.6.2 (Charge Enable)                                                |
 321    | MIL-PRF Requirements | The charge enable terminal shall comply with the following:       </br>\
 322                              ⠀⠀a. Maximum charge without enable: 400 mA                       </br>\
 323                              ⠀⠀b. Equivalent resistor: 235 Ω                                  </br>\
 324                              ⠀⠀c. Equivalent diode VF: 1.3 V                                  </br>\
 325                              ⠀⠀d. Approximate activation current: 7 mA                             |
 326    | Instructions         | 1. Increment charge current every second                          </br>\
 327                             2. Stop when charge current drops to ~0A                               |
 328    | Pass / Fail Criteria | Pass if the highest current is less than 400mA                         |
 329    | Estimated Duration   | 5 minutes                                                              |
 330    """
 331    failing_current = 0.400
 332    uncertainty = 0.005
 333
 334    # Enable charging and timer
 335    _plateset.ce_switch = False
 336    _bms.charger.set_profile(volts=16.8, amps=0.010)  # Starting current
 337    _bms.charger.enable()
 338    _bms.timer.reset()  # Keep track of runtime
 339
 340    # Charge until current drops below some threshold (can float above 0) or is more than failing_current
 341    max_charge_current = 0
 342    while (
 343        _bms.charger.target_amps <= failing_current + uncertainty
 344        and abs(_bms.charger.target_amps - (latest_current := _bms.charger.amps)) <= uncertainty
 345    ):
 346        # while (failing_current + uncertainty * 2) > (latest_current := _bms.charger.amps) > 0.005:
 347        max_charge_current = max(max_charge_current, latest_current)
 348        logger.write_info_to_report(
 349            f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Voltage: {_bms.dmm.volts:.3f}, "
 350            f"Current: {latest_current:.3f}"
 351        )
 352        # Increase by 1 mA / second
 353        _bms.charger.set_profile(volts=_bms.charger.target_volts, amps=_bms.charger.target_amps + 0.001)
 354        time.sleep(1)
 355
 356    # Charging is complete, turn off the charger
 357    _bms.charger.disable()
 358
 359    # Check results
 360    logger.write_result_to_html_report(f"Current: {max_charge_current:.3f} A ≤ {failing_current:.3f} ± {uncertainty} A")
 361    if max_charge_current > failing_current + uncertainty:
 362        pytest.fail(f"Current of {max_charge_current:.3f} A exceeds {failing_current:.3f} A limit.")
 363
 364
 365def test_taf_charge_enable_off():
 366    """
 367    | Description          | TAF: Confirm charging above 20mA doesn't work when CE is inactive      |
 368    | :------------------- | :--------------------------------------------------------------------- |
 369    | GitHub Issue         | turnaroundfactor/HITL#342                                       |
 370    | MIL-PRF Section      | 3.5.6.2 (Charge Enable)                                                |
 371    | Instructions         | 1. Increment charge current every second                          </br>\
 372                             2. Stop when charge current drops to ~0A                               |
 373    | Pass / Fail Criteria | Pass if the highest current is less than or equal to 20mA              |
 374    | Estimated Duration   | 1 minute                                                               |
 375    | Note                 | We want to disable if current is over 20 mA since                      \
 376                             that's what BT does, and it's a safer way to charge. Other OTS         \
 377                             may have different cutoffs, and the BT one was measured by experiment. |
 378    """
 379    target_current = 0.020
 380    uncertainty = 0.005
 381
 382    # Enable charging and timer
 383    _plateset.ce_switch = False
 384    _bms.charger.set_profile(volts=16.8, amps=0.010)  # Starting current
 385    _bms.charger.enable()
 386    _bms.timer.reset()  # Keep track of runtime
 387
 388    # Charge until current drops below some threshold (can float above 0) or is more than failing_current
 389    max_charge_current = 0
 390    while (
 391        _bms.charger.target_amps <= target_current + uncertainty
 392        and abs(_bms.charger.target_amps - (latest_current := _bms.charger.amps)) <= uncertainty
 393    ):
 394        max_charge_current = max(max_charge_current, latest_current)
 395        logger.write_info_to_report(
 396            f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Voltage: {_bms.dmm.volts:.3f}, "
 397            f"Current: {latest_current:.3f}"
 398        )
 399        # Increase by 1 mA / second
 400        _bms.charger.set_profile(volts=_bms.charger.target_volts, amps=_bms.charger.target_amps + 0.001)
 401        time.sleep(1)
 402
 403    # Charging is complete, turn off the charger
 404    _bms.charger.disable()
 405
 406    # Check results
 407    logger.write_result_to_html_report(f"Current: {max_charge_current:.3f} A = {target_current:.3f} ± {uncertainty} A")
 408    if not target_current - uncertainty <= max_charge_current <= target_current + uncertainty:
 409        pytest.fail(f"Current of {max_charge_current:.3f} A exceeds {target_current:.3f} A limit.")
 410
 411
 412@pytest.mark.parametrize("reset_test_environment", [{"volts": 2.5}], indirect=True)
 413class TestExtendedCycle:
 414    """Perform a long charge / discharge."""
 415
 416    class CellVoltageDiscrepancy(CSVRecordEvent):
 417        """@private Compare cell sim voltage to reported cell voltage."""
 418
 419        allowable_error = 0.01
 420        max = SimpleNamespace(cell_id=0, sim_v=0, bms_v=0, error=0.0)
 421
 422        @classmethod
 423        def failed(cls) -> bool:
 424            """Check if test parameters were exceeded."""
 425            return bool(cls.max.error > cls.allowable_error)
 426
 427        @classmethod
 428        def verify(cls, row, serial_data, _cell_data):
 429            """Cell voltage within range"""
 430            for i, cell_id in enumerate(_bms.cells):
 431                row_data = SimpleNamespace(
 432                    cell_id=cell_id,
 433                    sim_v=row[f"ADC Plate Cell {cell_id} Voltage (V)"],
 434                    bms_v=serial_data[f"mvolt_cell{'' if i == 0 else i}"] / 1000,
 435                )
 436                row_data.error = abs((row_data.bms_v - row_data.sim_v) / row_data.sim_v)
 437                cls.max = max(cls.max, row_data, key=lambda data: data.error)
 438
 439        @classmethod
 440        def result(cls):
 441            """Detailed test result information."""
 442            return (
 443                f"Cell Voltage error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)} "
 444                f"(Sim {cls.max.cell_id}: {cls.max.sim_v * 1000:.1f} mv, BMS: {cls.max.bms_v * 1000:.1f} mv)"
 445            )
 446
 447    class CurrentDiscrepancy(CSVRecordEvent):
 448        """@private Compare terminal current to reported current."""
 449
 450        allowable_error = 0.015
 451        max = SimpleNamespace(hitl_a=0, bms_a=0, error=0.0)
 452
 453        @classmethod
 454        def failed(cls) -> bool:
 455            """Check if test parameters were exceeded."""
 456            return bool(cls.max.error > cls.allowable_error)
 457
 458        @classmethod
 459        def verify(cls, row, serial_data, _cell_data):
 460            """Current within range"""
 461            row_data = SimpleNamespace(hitl_a=row["HITL Current (A)"], bms_a=serial_data["mamps"] / 1000)
 462            row_data.error = abs((row_data.bms_a - row_data.hitl_a) / row_data.hitl_a)
 463            if abs(row_data.hitl_a) > 0.100:  # Ignore currents within 100mA to -100mA
 464                cls.max = max(cls.max, row_data, key=lambda data: data.error)
 465
 466        @classmethod
 467        def result(cls):
 468            """Detailed test result information."""
 469            return (
 470                f"Current error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)} "
 471                f"(HITL: {cls.max.hitl_a * 1000:.1f} mA, BMS: {cls.max.bms_a * 1000:.1f} mA)"
 472            )
 473
 474    class TerminalVoltageDiscrepancy(CSVRecordEvent):
 475        """@private Compare HITL voltage to reported Terminal voltage."""
 476
 477        allowable_error = 0.015
 478        max = SimpleNamespace(hitl_v=0, bms_v=0, error=0.0)
 479
 480        @classmethod
 481        def failed(cls) -> bool:
 482            """Check if test parameters were exceeded."""
 483            return bool(cls.max.error > cls.allowable_error)
 484
 485        @classmethod
 486        def verify(cls, row, serial_data, _cell_data):
 487            """Terminal voltage within range"""
 488            row_data = SimpleNamespace(hitl_v=row["HITL Voltage (V)"], bms_v=serial_data["mvolt_terminal"] / 1000)
 489            row_data.error = abs((row_data.bms_v - row_data.hitl_v) / row_data.hitl_v)
 490            cls.max = max(cls.max, row_data, key=lambda data: data.error)
 491
 492        @classmethod
 493        def result(cls):
 494            """Detailed test result information."""
 495            return (
 496                f"Terminal Voltage error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)} "
 497                f"(HITL: {cls.max.hitl_v * 1000:.1f} mv, BMS: {cls.max.bms_v * 1000:.1f} mv)"
 498            )
 499
 500    class TemperatureDiscrepancyTherm1(CSVRecordEvent):
 501        """@private Compare HITL temperature to reported temperature."""
 502
 503        allowable_error = 5.0
 504        max = SimpleNamespace(hitl_c=0, bms_c=0, error=0.0)
 505
 506        @classmethod
 507        def failed(cls) -> bool:
 508            """Check if test parameters were exceeded."""
 509            return bool(cls.max.error > cls.allowable_error)
 510
 511        @classmethod
 512        def verify(cls, _row, serial_data, _cell_data):
 513            """Temperature within range"""
 514            row_data = SimpleNamespace(hitl_c=_plateset.thermistor1, bms_c=serial_data["dk_temp"] / 10 - 273)
 515            row_data.error = abs(row_data.bms_c - row_data.hitl_c)
 516            cls.max = max(cls.max, row_data, key=lambda data: data.error)
 517
 518        @classmethod
 519        def result(cls):
 520            """Detailed test result information."""
 521            return (
 522                f"Thermistor 1 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error, '°C', '.2f')}"
 523                f"(HITL: {cls.max.hitl_c:.2f} °C, BMS: {cls.max.bms_c:.2f} °C)"
 524            )
 525
 526    class TemperatureDiscrepancyTherm2(CSVRecordEvent):
 527        """@private Compare HITL temperature to reported temperature."""
 528
 529        allowable_error = 5.0
 530        max = SimpleNamespace(hitl_c=0, bms_c=0, error=0.0)
 531
 532        @classmethod
 533        def failed(cls) -> bool:
 534            """Check if test parameters were exceeded."""
 535            return bool(cls.max.error > cls.allowable_error)
 536
 537        @classmethod
 538        def verify(cls, _row, serial_data, _cell_data):
 539            """Temperature within range"""
 540            row_data = SimpleNamespace(hitl_c=_plateset.thermistor2, bms_c=serial_data["dk_temp1"] / 10 - 273)
 541            row_data.error = abs(row_data.bms_c - row_data.hitl_c)
 542            cls.max = max(cls.max, row_data, key=lambda data: data.error)
 543
 544        @classmethod
 545        def result(cls):
 546            """Detailed test result information."""
 547            return (
 548                f"Thermistor 2 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error, '°C', '.2f')}"
 549                f"(HITL: {cls.max.hitl_c:.2f} °C, BMS: {cls.max.bms_c:.2f} °C)"
 550            )
 551
 552    class NoFaults(CSVRecordEvent):
 553        """@private Check for any faults."""
 554
 555        faults: list[str] = []
 556
 557        @classmethod
 558        def failed(cls) -> bool:
 559            """Check if test parameters were exceeded."""
 560            return len(cls.faults) != 0
 561
 562        @classmethod
 563        def verify(cls, _row, serial_data, _cell_data):
 564            """Check for faults."""
 565            for key in serial_data:
 566                if "fault" in key and key.startswith("flags.") and serial_data[key]:
 567                    cls.faults.append(key.removeprefix("flags.").title())
 568
 569        @classmethod
 570        def result(cls):
 571            """Detailed test result information."""
 572            return f"Faults encountered: {' | '.join(cls.faults) or None}"
 573
 574    class NoReset(CSVRecordEvent):
 575        """@private Check for resets."""
 576
 577        reset_reasons = ControlStatusRegister(0)
 578
 579        @classmethod
 580        def failed(cls) -> bool:
 581            """Check if test parameters were exceeded."""
 582            return bool(cls.reset_reasons)
 583
 584        @classmethod
 585        def verify(cls, _row, serial_data, _cell_data):
 586            """Check if any resets occurred."""
 587            reset_reason = ControlStatusRegister(serial_data.get("Reset_Flags", 0))
 588            reset_reason &= ~ControlStatusRegister.POWER & ~ControlStatusRegister.RESET_PIN  # Ignore valid reasons
 589            cls.reset_reasons |= reset_reason
 590
 591        @classmethod
 592        def result(cls):
 593            """Detailed test result information."""
 594            return f"Resets encountered: {str(cls.reset_reasons) or None}"
 595
 596    class SOCDiscrepancy(CSVRecordEvent):
 597        """@private Compare lowest HITL cell SOC to reported SOC."""
 598
 599        allowable_error = 0.05
 600        max = SimpleNamespace(sim_id=0, sim_soc=0, bms_soc=0, error=0.0)
 601
 602        @classmethod
 603        def failed(cls) -> bool:
 604            """Check if test parameters were exceeded."""
 605            return bool(cls.max.error > cls.allowable_error)
 606
 607        @classmethod
 608        def verify(cls, _row, serial_data, cell_data):
 609            """SOC within range."""
 610            lowest_sim_soc_id = min(cell_data, key=lambda cell_id: cell_data[cell_id]["state_of_charge"])
 611            lowest_sim_soc = cell_data[lowest_sim_soc_id]["state_of_charge"]
 612            row_data = SimpleNamespace(
 613                sim_id=lowest_sim_soc_id, sim_soc=lowest_sim_soc, bms_soc=serial_data["percent_charged"] / 100
 614            )
 615            row_data.error = abs(row_data.bms_soc - row_data.sim_soc)
 616            cls.max = max(cls.max, row_data, key=lambda data: data.error)
 617
 618        @classmethod
 619        def result(cls):
 620            """Detailed test result information."""
 621            return (
 622                f"SOC error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)} "
 623                f"(Sim {cls.max.sim_id}: {cls.max.sim_soc:.1%}, BMS: {cls.max.bms_soc:.1%})"
 624            )
 625
 626    class HealthChangeCount(CSVRecordEvent):
 627        """@private Check how many times Health changes."""
 628
 629        changes = 0
 630        allowable_changes = 1
 631        last_reported_health: int | None = None
 632
 633        @classmethod
 634        def failed(cls) -> bool:
 635            """Check if test parameters were exceeded."""
 636            return bool(cls.changes > cls.allowable_changes)
 637
 638        @classmethod
 639        def verify(cls, _row, serial_data, _cell_data):
 640            """Detect health change."""
 641            if serial_data["percent_health"] != cls.last_reported_health:
 642                cls.changes += cls.last_reported_health is not None
 643                cls.last_reported_health = serial_data["percent_health"]
 644
 645        @classmethod
 646        def result(cls):
 647            """Detailed test result information."""
 648            return f"Health change#: {cls.cmp(cls.changes, '<=', cls.allowable_changes, form='d')}"
 649
 650    class HealthChange(CSVRecordEvent):
 651        """@private Check health change."""
 652
 653        allowable_change = 0.01
 654        max_change: float = 0.0
 655        initial_health: float | None = None
 656
 657        @classmethod
 658        def failed(cls) -> bool:
 659            """Check if test parameters were exceeded."""
 660            return bool(cls.max_change > cls.allowable_change)
 661
 662        @classmethod
 663        def verify(cls, _row, serial_data, _cell_data):
 664            """Health change within range."""
 665            if cls.initial_health is None:
 666                cls.initial_health = serial_data["percent_health"] / 100
 667            cls.max_change = max(cls.max_change, abs(cls.initial_health - serial_data["percent_health"] / 100))
 668
 669        @classmethod
 670        def result(cls):
 671            """Detailed test result information."""
 672            return f"Health change: {cls.cmp(cls.max_change, '<=', cls.allowable_change)}"
 673
 674    class UsedAhDiscrepancy(CSVRecordEvent):
 675        """@private Compare HITL used Ah to reported used Ah."""
 676
 677        allowable_error = 0.01
 678        max = SimpleNamespace(hitl_ah=0, bms_ah=0, error=0.0)
 679        initial_charge_cycle: int | None = None
 680
 681        @classmethod
 682        def failed(cls) -> bool:
 683            """Check if test parameters were exceeded."""
 684            return bool(cls.max.error > cls.allowable_error)
 685
 686        @classmethod
 687        def verify(cls, row, serial_data, _cell_data):
 688            """Check used Ah during discharge."""
 689            if cls.initial_charge_cycle is None:
 690                cls.initial_charge_cycle = serial_data["charge_cycles"]
 691            delta_cycle = serial_data["charge_cycles"] - cls.initial_charge_cycle
 692            row_data = SimpleNamespace(
 693                hitl_ah=-(row["HITL Capacity (Ah)"] or 0),
 694                bms_ah=(serial_data["milliamp_hour_used"] + delta_cycle * serial_data["milliamp_hour_capacity"]) / 1000,
 695            )
 696            if row["Cycle"] == "run_discharge_cycle" and row_data.hitl_ah > 0.100:
 697                logger.write_debug_to_report(f"  In discharge: {row_data.hitl_ah} Ah")
 698                row_data.error = abs((row_data.bms_ah - row_data.hitl_ah) / row_data.hitl_ah)
 699                cls.max = max(cls.max, row_data, key=lambda data: data.error)
 700
 701        @classmethod
 702        def result(cls):
 703            """Detailed test result information."""
 704            return (
 705                f"Used Ah error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)} "
 706                f"(HITL: {cls.max.hitl_ah * 1000:.1f} mAh, BMS: {cls.max.bms_ah * 1000:.1f} mAh)"
 707            )
 708
 709    class ChargeCycle(CSVRecordEvent):
 710        """@private Compare cell sim voltage to reported cell voltage."""
 711
 712        allowable_cycles = 1
 713        initial_cycle: int | None = None
 714        max_cycle = 0
 715
 716        @classmethod
 717        def failed(cls) -> bool:
 718            """Check if test parameters were exceeded."""
 719            return cls.initial_cycle is None or bool(cls.max_cycle - cls.initial_cycle > cls.allowable_cycles)
 720
 721        @classmethod
 722        def verify(cls, _row, serial_data, _cell_data):
 723            """Cell voltage within range"""
 724            if cls.initial_cycle is None:
 725                cls.initial_cycle = serial_data["charge_cycles"]
 726            cls.max_cycle = max(cls.max_cycle, serial_data["charge_cycles"])
 727
 728        @classmethod
 729        def result(cls):
 730            """Detailed test result information."""
 731            return (
 732                f"Cycle change: "
 733                f"{cls.cmp(cls.max_cycle - (cls.initial_cycle or 0), '<=', cls.allowable_cycles, form='d')} "
 734                f"(Starting: {cls.initial_cycle}, Ending: {cls.max_cycle})"
 735            )
 736
 737    def test_extended_cycle(self):
 738        """
 739        | Description          | Perform a long charge / discharge                                      |
 740        | :------------------- | :--------------------------------------------------------------------- |
 741        | GitHub Issue         | turnaroundfactor/HITL#342                                       |
 742        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
 743jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
 744        | MIL-PRF Sections     | 4.7.2.3 (Capacity discharge)                                      </br>\
 745                                 4.6.1 (Standard Charge)                                           </br>\
 746                                 4.3.1 (Normal conditions)                                         </br>\
 747                                 3.5.3 (Capacity)                                                       |
 748        | Instructions         | 1. Set thermistors to 23C                                         </br>\
 749                                 2. Put cells in a rested state at 2.5V per cell                   </br>\
 750                                 3. Charge battery (16.8V / 3A / 100 mA cutoff)                    </br>\
 751                                 4. Rest for 3.1 hours (we want to calculate SoH change which requires 3 hours) </br>\
 752                                 5. Discharge at 2A until battery reaches 10V then stop discharging     |
 753        | Pass / Fail Criteria | ⦁ Serial cell voltage agrees with HITL cell voltage to within 1%  </br>\
 754                                 ⦁ Serial terminal current agrees with HITL current to within 1%   </br>\
 755                                 ⦁ Serial terminal voltage agrees with HITL voltage to within 1%   </br>\
 756                                 ⦁ Serial thermistor 1 and 2 agree with HITL thermistor 1 and 2 to within 5°C </br>\
 757                                 ⦁ No Fault Flags over entire duration of test                     </br>\
 758                                 ⦁ No resets occur over entire duration of test                    </br>\
 759                                 ⦁ Serial cell SOC agrees with lowest HITL cell SOC to within 5% SOC </br>\
 760                                 ⦁ Serial Health shall only change once                            </br>\
 761                                 ⦁ Serial Health shall not change by more than 1%                  </br>\
 762                                 ⦁ Serial used Ah agrees with HITL Ah to within 1% (for abs(HITL Ah > 100mAh))  </br>\
 763                                 ⦁ Serial charge cycle shall only increment once                   </br>\
 764                                 ⦁ E-ink display is operational [TBD How to do this]                    |
 765        | Estimated Duration   | 12 hours                                                               |
 766        | Note                 | MIL-PRF 4.7.2.3.1 (Initial capacity discharge): Each battery subjected \
 767                                 to the capacity discharge test above (see 4.7.2.3) on its initial      \
 768                                 charge/discharge cycle is permitted up to three cycles to meet the     \
 769                                 capacity discharge test requirement (see 3.1). Any battery not meeting \
 770                                 the specified capacity discharge requirement (see 3.1) during any of   \
 771                                 its first three cycles is considered a failure                         |
 772        """
 773        # FIXME(JA): adjust estimated duration based on first test
 774
 775        standard_charge()
 776        standard_rest(seconds=30 if FAST_MODE else 3.1 * 3600)
 777        standard_discharge()
 778
 779        # Check results
 780        if CSVRecordEvent.failed():  # FIXME(JA): make this implicit?
 781            pytest.fail(CSVRecordEvent.result())
 782
 783
 784@pytest.mark.parametrize("reset_test_environment", [{"volts": 3.7}], indirect=True)
 785class TestSMBusWritableRegisters:
 786    """SMBus Writable Registers"""
 787
 788    def test_smbus_writable_registers(self) -> None:
 789        """
 790        | Description          | Validate SMBus Writable Registers                                      |
 791        | :------------------- | :--------------------------------------------------------------------- |
 792        | GitHub Issue         | turnaroundfactor/HITL#397                                       |
 793        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
 794jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
 795        | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
 796        | Instructions         | 1. Set thermistors to 23C                                         </br>\
 797                                 2. Put cells in a rested state at 3.7V per cell                   </br>\
 798                                 3. Host to Battery Read Word (Register 1): 0x00                   </br>\
 799                                 5. Host to Battery Read Word (Register 2): 0x0438                 </br>\
 800                                 6. Host to Battery Write Word (Register 2): 0xBEEF                </br>\
 801                                 7. Host to Battery Read Word (Register 2): 0xBEEF                 </br>\
 802                                 8. Host to Battery Read Word (Register 3): 0x000A                 </br>\
 803                                 9. Host to Battery Write Word (Register 3): 0xBEEF                </br>\
 804                                 10. Host to Battery Read Word (Register 3): 0xBEEF                </br>\
 805                                 11. Host to Battery Write Word (Register 3): 0x000A               </br>\
 806                                 12. Host to Battery Read Word (Register 3): 0x000A                </br>\
 807                                 13. Host to Battery Read Word (Register 4): 000xx00x0010 (where x is "don't care") |
 808        | Pass / Fail Criteria | ⦁ ManufacturerAccess (Battery Register 1) returns 0x00            </br>\
 809                                 ⦁ Remaining Capacity Alarm (Battery Register 2) returns 0x0438    </br>\
 810                                 ⦁ Remaining Capacity Alarm (Battery Register 2) returns 0xBEEF    </br>\
 811                                 ⦁ Remaining Time Alarm (Battery Register 3) returns 0x000A        </br>\
 812                                 ⦁ Remaining Time Alarm (Battery Register 3) returns 0xBEEF        </br>\
 813                                 ⦁ Remaining Time Alarm (Battery Register 3) returns 0x000A        </br>\
 814                                 ⦁ Battery Mode (Battery Register 4) returns 000xx00x0010               |
 815        | Estimated Duration   | 30 seconds                                                              |
 816        | Note                 | When specified (see 3.1), batteries shall be compliant with System     \
 817                                 Management Bus (SMBus) Specification Revision 1.1 and Smart Battery    \
 818                                 Data (SBData) Specification, version 1.1, with the exception that      \
 819                                 SBData safety signal hardware requirements therein shall be replaced   \
 820                                 with a charge enable when a charge enable is specified (see 3.1 and    \
 821                                 3.5.6). Certification is required. Batteries shall be compatible       \
 822                                 with appropriate Level 2 and Level 3 chargers (see 6.4.7).             \
 823                                 When tested as specified in 4.7.2.15.1, SMBus data output shall be     \
 824                                 accurate within +0/-5% of the actual state of charge for the battery   \
 825                                 under test throughout the discharge. Manufacturer and battery data     \
 826                                 shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V     \
 827                                 logic circuitry. Pull-up resistors will be provided by the charger.    \
 828                                 SMBus circuitry shall respond to a SMBus query within 2 seconds.       \
 829                                 Smart batteries may act as master or slave on the bus, but must        \
 830                                 perform bus master timing arbitration according to the SMBus           \
 831                                specification when acting as master.                                    |
 832        """
 833        time.sleep(30)
 834
 835        assert (serial_data := serial_monitor.read())
 836        ten_percent_capacity = float(serial_data["milliamp_hour_capacity"]) // 10
 837        default_value = 0x000A
 838        easy_spot_value = 0xBEEF
 839        smbus_registers = [
 840            {"sm_register": SMBusReg.MANUFACTURING_ACCESS, "data": [0]},
 841            {
 842                "sm_register": SMBusReg.REMAINING_CAPACITY_ALARM,
 843                "data": [ten_percent_capacity, easy_spot_value],
 844            },
 845            {
 846                "sm_register": SMBusReg.REMAINING_TIME_ALARM,
 847                "data": [default_value, easy_spot_value, default_value],
 848            },
 849        ]
 850        failed_tests = []
 851
 852        # Manufacturing Access, Remaining Capacity Alarm, Remaining Time Alarm
 853        for smbus_register in smbus_registers:
 854            register = cast(SMBusReg, smbus_register["sm_register"])
 855            index = 0
 856            for elem in map(int, cast(list[int], smbus_register["data"])):
 857                if index > 0:
 858                    logger.write_info_to_report(f"Writing {hex(elem)} to {register.fname}")
 859                    _smbus.write_register(register, elem)
 860
 861                read_sm_response = _smbus.read_register(register)
 862
 863                expected_value = elem.to_bytes(2, byteorder="little")
 864                if read_sm_response[1] != expected_value:
 865                    logger.write_result_to_html_report(
 866                        f"{register.fname} did not have expected result of {hex(elem)}, "
 867                        f"instead was: {read_sm_response[1]!r}"
 868                    )
 869                    logger.write_warning_to_report(
 870                        f"{register.fname} did not have expected result of {hex(elem)}, "
 871                        f"instead was: {read_sm_response[1]!r}"
 872                    )
 873                    failed_tests.append(register.fname)
 874                elif index == 0:
 875                    message = f"{register.fname} passed reading default value {hex(elem)}"
 876                    logger.write_result_to_html_report(message)
 877                    logger.write_info_to_report(message)
 878                else:
 879                    message = f"{register.fname} had value correctly changed to {hex(elem)}"
 880                    logger.write_result_to_html_report(message)
 881                    logger.write_info_to_report(message)
 882
 883                index += 1
 884
 885        # BatteryMode
 886        # TODO: BatteryMode will be updated at later date
 887
 888        # mask = 0b111001101111
 889        # pattern = 0b000000000010  # 000xx00x0010
 890        battery_mode = SMBusReg.BATTERY_MODE
 891        read_bm_response = _smbus.read_register(battery_mode)
 892        # bm_bytes = int.from_bytes(read_bm_response[1], byteorder="little")
 893        # logger.write_info_to_report(f"Bytes of {battery_mode.fname}: {bm_bytes}")
 894
 895        # masked_response = mask & bm_bytes
 896        expected_value = 0x0000.to_bytes(2, byteorder="little")
 897        if read_bm_response[1] == expected_value:
 898            logger.write_info_to_report(f"{battery_mode.fname} passed response check")
 899        else:
 900            logger.write_warning_to_report(f"{battery_mode.fname} did not have expected result of: 0x0000")
 901            failed_tests.append(battery_mode.fname)
 902
 903        # Overall results report
 904        if failed_tests:
 905            failed_registers = list(dict.fromkeys(failed_tests))
 906            message = f"{len(failed_registers)} register(s) failed at least one test: {', '.join(failed_registers)}"
 907            logger.write_result_to_html_report(f"<font color='#990000'> {message}</font>")
 908            pytest.fail(message)
 909        else:
 910            logger.write_result_to_html_report("All tested registers passed writable test")
 911
 912
 913def analyze_register_response(
 914    requirements: list[dict[str, SMBusReg | float] | dict[str, SMBusReg | int] | dict[str, SMBusReg | bool]],
 915    failed_list: list[str],
 916    at_rate: int,
 917) -> list[str]:
 918    """Reads SMBus register response and validates"""
 919
 920    full_amount = 0
 921    remaining_amount = 0
 922
 923    for elem in requirements:
 924        register: SMBusReg = cast(SMBusReg, elem["register"])
 925        requirement = elem["requirement"]
 926
 927        read_response = _smbus.read_register(register)
 928        if requirement is True:
 929            if not read_response[0]:
 930                message = f"Invalid response for {register.fname}. Expected non-zero value, but got {read_response[0]}"
 931                logger.write_warning_to_report(message)
 932                failed_list.append(f"{register.fname} after setting AtRate value to {at_rate}(mA)")
 933            message = f"Received valid response for {register.fname}."
 934            logger.write_info_to_report(message)
 935
 936        else:
 937            assert isinstance(read_response[0], int | float) and isinstance(requirement, int | float)
 938            if not math.isclose(read_response[0], requirement, rel_tol=0.1):
 939                message = f"Invalid response for {register.fname}. Expected {requirement}, but got {read_response[0]}"
 940                logger.write_warning_to_report(message)
 941                failed_list.append(f"{register.fname} after setting AtRate value to {at_rate}(mA)")
 942
 943            message = f"Received valid response for {register.fname}"
 944            logger.write_info_to_report(message)
 945
 946        if register == SMBusReg.AT_RATE_TIME_TO_FULL:
 947            assert isinstance(read_response[0], int)
 948            full_amount = read_response[0]
 949
 950        if register == SMBusReg.AT_RATE_TIME_TO_EMPTY:
 951            assert isinstance(read_response[0], int)
 952            remaining_amount = read_response[0]
 953
 954    logger.write_result_to_html_report(f"{at_rate} AtRate, {remaining_amount} Remaining, {full_amount} Full")
 955    return failed_list
 956
 957
 958# @pytest.mark.parametrize("reset_test_environment", [{"volts": 3.5, "temperature": 15, "soc": 0.80}], indirect=True)
 959class TestSMBusAtRate:
 960    """Validate the SMBus AtRate Commands"""
 961
 962    def test_smbus_at_rate(self):
 963        # TODO: Update Documentation
 964        """
 965        | Description          | Validate SMBus AtRate Commands                                         |
 966        | :------------------- | :--------------------------------------------------------------------- |
 967        | GitHub Issue         | turnaroundfactor/HITL#398                                       |
 968        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
 969jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
 970        | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
 971        | Instructions         | 1. Write word 1000 (unit is mA) to AtRate[Register 0x4]           </br>\
 972                                 2. Read word from AtRateTimeToFull[0x5]                           </br>\
 973                                 3. Read word from AtRate TimeToEmpty[0x6]                         </br>\
 974                                 4. Read word from AtRateOK[0x7]                                   </br>\
 975                                 5. Write word -1000 to AtRate[Register 0x4]                       </br>\
 976                                 6. Read word from AtRateTimeToFull[0x5]                           </br>\
 977                                 7. Read word from AtRateTimeToEmpty[0x6]                          </br>\
 978                                 8. Charge battery at 2A                                           </br>\
 979                                 9. Read word from AtRateOK[0x7]                                   </br>\
 980                                 10. Charge battery at 0.1A                                        </br>\
 981                                 11. Read word from AtRateOK[0x7]                                  </br>\
 982                                 12. Discharge battery at 2A                                       </br>\
 983                                 13. Read word from AtRateOK[0x7]                                  </br>\
 984                                 14. Discharge battery at 0.1A                                     </br>\
 985                                 15. Read word from AtRateOK[0x7]                                  </br>\
 986                                 16. Write word 0 to AtRate[Register 0x4]                          </br>\
 987                                 17. Read word from AtRateTimeToFull[0x6]                          </br>\
 988                                 18. Read word from AtRateTimeToEmpty[0x6]                         </br>\
 989                                 19. Read word from AtRateOK[0x7]                                  </br>\
 990                                 20. Write word 0x8000 to BatteryMode[Register 0x3]                </br>\
 991                                 21. Write word 0x0808 to BatteryMode[Register 0x3]                     |
 992        | Pass / Fail Criteria | Charging (mA) ----                                                </br>\
 993                                 ⦁ Expect AtRateTimeToFull to be FullChargeCapacity-RemainingCapacity/1000/60 </br>\
 994                                 ⦁ Expect AtRateTimeToEmpty to be 65,635                           </br>\
 995                                 ⦁ Expect AtRateOK to be True (non-zero)                           </br>\
 996                                 Discharging(ma) ----                                              </br>\
 997                                 ⦁ Expect AtRateTimeToFull to be 65,535                            </br>\
 998                                 ⦁ Expect AtRateTimeToEmpty to be RemainingCapacity / 1000 / 60    </br>\
 999                                 ⦁ Expect AtRateOK to be True (non-zero) for all charge changes    </br>\
1000                                 Rest (mA) ----                                                    </br>\
1001                                 ⦁ Expect AtRAteTimeToFull to be 65,535                            </br>\
1002                                 ⦁ Expect AtRateTimeToEmpty to be 65,535                           </br>\
1003                                 ⦁ Expect AtRateOK to be True (non-zero)                                |
1004        | Estimated Duration   | 10 seconds                                                             |
1005        | Note                 | When specified (see 3.1), batteries shall be compliant with System     \
1006                                 Management Bus (SMBus) Specification Revision 1.1 and Smart Battery    \
1007                                 Data (SBData) Specification, version 1.1, with the exception that      \
1008                                 SBData safety signal hardware requirements therein shall be replaced   \
1009                                 with a charge enable when a charge enable is specified (see 3.1 and    \
1010                                 3.5.6). Certification is required. Batteries shall be compatible       \
1011                                 with appropriate Level 2 and Level 3 chargers (see 6.4.7).             \
1012                                 When tested as specified in 4.7.2.15.1, SMBus data output shall be     \
1013                                 accurate within +0/-5% of the actual state of charge for the battery   \
1014                                 under test throughout the discharge. Manufacturer and battery data     \
1015                                 shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V     \
1016                                 logic circuitry. Pull-up resistors will be provided by the charger.    \
1017                                 SMBus circuitry shall respond to a SMBus query within 2 seconds.       \
1018                                 Smart batteries may act as master or slave on the bus, but must        \
1019                                 perform bus master timing arbitration according to the SMBus           \
1020                                specification when acting as master.                                    |
1021        """
1022        time.sleep(30)
1023
1024        at_rate_register = SMBusReg.AT_RATE
1025        at_rate_time_to_full_register = SMBusReg.AT_RATE_TIME_TO_FULL
1026        at_rate_time_to_empty_register = SMBusReg.AT_RATE_TIME_TO_EMPTY
1027        at_rate_ok_register = SMBusReg.AT_RATE_OK
1028        #
1029        failed_tests = []
1030
1031        #
1032        # # Charging(mA)
1033        logger.write_info_to_report("Setting AtRate value to 1000(mA)")
1034        _smbus.write_register(at_rate_register, 1000)
1035        time.sleep(2)
1036        at_rate_response = _smbus.read_register(at_rate_register)
1037        logger.write_info_to_report(f"AtRate is now: {at_rate_response}")
1038
1039        full_charge = _smbus.read_register(SMBusReg.FULL_CHARGE_CAPACITY)
1040        remaining_capacity = _smbus.read_register(SMBusReg.REMAINING_CAPACITY)
1041        calculated_time_to_full = (full_charge[0] - remaining_capacity[0]) / 1000 * 60 / 0.975
1042
1043        charging_elems = [
1044            {"register": at_rate_time_to_full_register, "requirement": calculated_time_to_full},
1045            {"register": at_rate_time_to_empty_register, "requirement": 65535},
1046            {"register": at_rate_ok_register, "requirement": True},
1047        ]
1048
1049        failed_tests = analyze_register_response(charging_elems, failed_tests, at_rate_response[0])
1050
1051        # # Discharging
1052        logger.write_info_to_report("Setting AtRate value to -1000(mA)")
1053        _smbus.write_register(at_rate_register, -1000)
1054        time.sleep(2)
1055        at_rate_response = _smbus.read_register(at_rate_register)
1056        logger.write_info_to_report(f"AtRate is now: {at_rate_response}")
1057        remaining_capacity = _smbus.read_register(SMBusReg.REMAINING_CAPACITY)
1058        empty_requirement = remaining_capacity[0] / 1000 * 60 * 0.975
1059
1060        discharging_elems = [
1061            {"register": at_rate_time_to_full_register, "requirement": 65535},
1062            {"register": at_rate_time_to_empty_register, "requirement": empty_requirement},
1063        ]
1064
1065        failed_tests = analyze_register_response(discharging_elems, failed_tests, at_rate_response[0])
1066
1067        # AtRateOK -- Charging & Discharging
1068        rates = [
1069            {"charge": True, "rate": 2},
1070            {"charge": True, "rate": 0.1},
1071            {"charge": False, "rate": 2},
1072            {"charge": False, "rate": 0.1},
1073        ]
1074
1075        for elem in rates:
1076            if elem["charge"]:
1077                logger.write_info_to_report(f"Charging battery at {elem['rate']}A")
1078                with _bms.charger(16.8, elem["rate"]):
1079                    time.sleep(2)
1080                    at_rate_ok_response = _smbus.read_register(at_rate_ok_register)
1081                    if not at_rate_ok_response[0]:
1082                        message = (
1083                            f"Invalid response for {at_rate_ok_register.fname}. "
1084                            f"Expected non-zero value, but got {at_rate_ok_response[0]}"
1085                        )
1086                        logger.write_warning_to_report(message)
1087                        failed_tests.append(
1088                            f"{at_rate_ok_register.fname} after setting AtRate value to {at_rate_response[0]}(mA)"
1089                        )
1090                    else:
1091                        logger.write_result_to_html_report(
1092                            f"{at_rate_ok_register.fname} had expected value of True after charging at {elem['rate']}A"
1093                        )
1094            else:
1095                logger.write_info_to_report(f"Discharging battery at {elem['rate']}A")
1096                with _bms.load(elem["rate"]):
1097                    time.sleep(2)
1098                    at_rate_ok_response = _smbus.read_register(at_rate_ok_register)
1099                    if not at_rate_ok_response[0]:
1100                        message = (
1101                            f"Invalid response for {at_rate_ok_register.fname}. "
1102                            f"Expected non-zero value, but got {at_rate_ok_response[0]}"
1103                        )
1104                        logger.write_warning_to_report(message)
1105                        failed_tests.append(
1106                            f"{at_rate_ok_register.fname} after setting AtRate value to {at_rate_response[0]}(mA)"
1107                        )
1108                    else:
1109                        logger.write_result_to_html_report(
1110                            f"{at_rate_ok_register.fname} had expected value of True "
1111                            f"after discharging at {elem['rate']}A"
1112                        )
1113        # Rest(mA)
1114        logger.write_info_to_report("Setting AtRate value to 0(mA)")
1115        _smbus.write_register(at_rate_register, 0)
1116        time.sleep(2)
1117
1118        at_rate_response = _smbus.read_register(at_rate_register)
1119        logger.write_info_to_report(f"AtRate is now: {at_rate_response}")
1120        full_charge_three = _smbus.read_register(SMBusReg.FULL_CHARGE_CAPACITY)
1121        logger.write_info_to_report(f"{SMBusReg.FULL_CHARGE_CAPACITY.fname}: {full_charge_three}")
1122
1123        remaining_capacity_three = _smbus.read_register(SMBusReg.REMAINING_CAPACITY)
1124        logger.write_info_to_report(f"{SMBusReg.REMAINING_CAPACITY.fname}: {remaining_capacity_three}")
1125        resting_elems = [
1126            {"register": at_rate_time_to_full_register, "requirement": 65535},
1127            {"register": at_rate_time_to_empty_register, "requirement": 65535},
1128            {"register": at_rate_ok_register, "requirement": True},
1129        ]
1130
1131        failed_tests = analyze_register_response(resting_elems, failed_tests, at_rate_response[0])
1132
1133        # Power Mode
1134        battery_mode_register = SMBusReg.BATTERY_MODE
1135        writing_value = 0x8000
1136        print(f"Writing {writing_value} to {battery_mode_register.fname}")
1137        _smbus.write_register(battery_mode_register, writing_value)
1138        batt_response = _smbus.read_register(battery_mode_register)
1139        logger.write_info_to_report(f"{battery_mode_register.fname} after writing {writing_value}: {batt_response}")
1140        batt_bytes = batt_response[1]
1141        batt_analyzed = BatteryMode(batt_bytes)
1142        logger.write_info_to_report(f"Capacity Mode after writing {writing_value}: {batt_analyzed.capacity_mode}")
1143
1144        writing_value = 0x8080
1145        print(f"Writing {writing_value} to {battery_mode_register.fname}")
1146        _smbus.write_register(battery_mode_register, writing_value)
1147
1148        batt_mode_response = _smbus.read_register(battery_mode_register)
1149        logger.write_info_to_report(
1150            f"{battery_mode_register.fname} after writing {writing_value}: {batt_mode_response}"
1151        )
1152        batt_bytes = batt_mode_response[1]
1153        batt_analyzed = BatteryMode(batt_bytes)
1154        logger.write_info_to_report(f"Capacity Mode after writing {writing_value}: {batt_analyzed.capacity_mode}")
1155        if batt_analyzed.capacity_mode is not True:
1156            logger.write_warning_to_report(
1157                f"{battery_mode_register.fname} did not have Capacity Mode with expected "
1158                f"value of True, instead received {batt_analyzed.capacity_mode}"
1159            )
1160            failed_tests.append(f"{battery_mode_register.fname} did not have expected Capacity Mode value")
1161        else:
1162            logger.write_result_to_html_report(
1163                f"{battery_mode_register.fname} had Capacity Mode with expected value of {batt_analyzed.capacity_mode}"
1164            )
1165
1166        if failed_tests:
1167            message = f"{len(failed_tests)} AtRate tests failed: {', '.join(failed_tests)}"
1168            logger.write_warning_to_report(message)
1169            pytest.fail(f"<font color='#990000'>{message}</font>")
1170
1171        logger.write_result_to_html_report("All AtRate SMBus tests passed")
1172
1173
1174@pytest.mark.parametrize("reset_test_environment", [{"soc": 0.50}], indirect=True)
1175class TestConstantSMBusValues:
1176    """Test constant SMBus values"""
1177
1178    def test_constant_smbus_values(self):
1179        """
1180           | Description          | Constant SMBus Values                                                  |
1181           | :------------------- | :--------------------------------------------------------------------- |
1182           | GitHub Issue         | turnaroundfactor/HITL#385                                       |
1183           | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1184   jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D12) |
1185           | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
1186           | Instructions         | 1. Read Device Chemistry [0x22]                                   </br>\
1187                                    2. Read Device Name [0x21]                                        </br>\
1188                                    3. Read Manufacturer Name [0x20]                                  </br>\
1189                                    4. Read Serial Number [0x1C]                                      </br>\
1190                                    5. Read Manufacturer Date [0x1B]                                  </br>\
1191                                    6. Read Specification Info [0x1A]                                 </br>\
1192                                    7. Read Design Voltage [0x19]                                     </br>\
1193                                    8. Read Design Capacity [0x18]                                    </br>\
1194                                    9. Read Charging Voltage [0x15]                                   </br>\
1195                                    10. Read Charging Current [0x14]                                  </br>\
1196                                    11. Read Manufacturer Access [0x00]                               </br>\
1197                                    12. Read Remaining Time Alarm [0x02]                              </br>\
1198                                    13. Read Remaining Capacity Alarm [0x01]                          </br>\
1199                                    14. Read AtRateOk [0x07]                                          </br>\
1200                                    15. Read AtRateTimeToEmpty [0x06]                                 </br>\
1201                                    16. Read AtRateTimeToFull [0x05]                                  </br>\
1202                                    17. Read AtRate [0x04]                                            </br>\
1203                                    18. Read Battery Mode [0x03]                                      </br>\
1204                                    19. Read Max Error [0x0C]                                         </br>\
1205                                    20. Read Full Charge Capacity [0x10]                                   |
1206           | Pass / Fail Criteria | Charging (mA) ----                                                </br>\
1207                                    ⦁ Expect Device Chemistry [0x22] to be LION                       </br>\
1208                                    ⦁ Expect Device Name [0x21] to be BB-2590/U                       </br>\
1209                                    ⦁ Expect Manufacturer Name [0x20] to be TURN-AROUND FACTOR        </br>\
1210                                    ⦁ Expect Serial Number [0x1C] to be a unique value                </br>\
1211                                    ⦁ Expect Manufacturer Date [0x1B] to be 0x0100                    </br>\
1212                                    ⦁ Expect Specification Info [0x1A] to be 0x0100                   </br>\
1213                                    ⦁ Expect Design Voltage [0x19] to be 16800                        </br>\
1214                                    ⦁ Expect Design Capacity [0x18] to be CAPACITY * 0.975            </br>\
1215                                    ⦁ Expect Charging Voltage [0x15] to be 16800                      </br>\
1216                                    ⦁ Expect Charging Current [0x14] to be 2000                       </br>\
1217                                    ⦁ Expect Manufacturer Access [0x00] to be 0                       </br>\
1218                                    ⦁ Expect Remaining Time Alarm [0x02] to be 10                     </br>\
1219                                    ⦁ Expect Remaining Capacity Alarm [0x01] to be SOC * Design Capacity  </br>\
1220                                    ⦁ Expect AtRateOk [0x07] to be True                               </br>\
1221                                    ⦁ Expect AtRateTimeToEmpty [0x06] to be 65535                     </br>\
1222                                    ⦁ Expect AtRateTimeToFull [0x05] to be 65535                      </br>\
1223                                    ⦁ Expect AtRate [0x04] to be 0                                    </br>\
1224                                    ⦁ Expect Battery Mode [0x03] to be 0                              </br>\
1225                                    ⦁ Expect Max Error [0x0C] to be 0                                 </br>\
1226                                    ⦁ Expect Full Charge Capacity [0x10] to be CAPACITY                    |
1227           | Estimated Duration   | 10 seconds                                                             |
1228           | Note                 | When specified (see 3.1), batteries shall be compliant with System     \
1229                                    Management Bus (SMBus) Specification Revision 1.1 and Smart Battery    \
1230                                    Data (SBData) Specification, version 1.1, with the exception that      \
1231                                    SBData safety signal hardware requirements therein shall be replaced   \
1232                                    with a charge enable when a charge enable is specified (see 3.1 and    \
1233                                    3.5.6). Certification is required. Batteries shall be compatible       \
1234                                    with appropriate Level 2 and Level 3 chargers (see 6.4.7).             \
1235                                    When tested as specified in 4.7.2.15.1, SMBus data output shall be     \
1236                                    accurate within +0/-5% of the actual state of charge for the battery   \
1237                                    under test throughout the discharge. Manufacturer and battery data     \
1238                                    shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V     \
1239                                    logic circuitry. Pull-up resistors will be provided by the charger.    \
1240                                    SMBus circuitry shall respond to a SMBus query within 2 seconds.       \
1241                                    Smart batteries may act as master or slave on the bus, but must        \
1242                                    perform bus master timing arbitration according to the SMBus           \
1243                                   specification when acting as master.                                    |
1244        """
1245        constant_elements = [
1246            {"register": SMBusReg.DEVICE_CHEMISTRY, "requirement": "LION"},
1247            {"register": SMBusReg.DEVICE_NAME, "requirement": "BB-2590/U"},
1248            {"register": SMBusReg.MANUFACTURER_NAME, "requirement": "TURN-AROUND FACTOR"},
1249            {"register": SMBusReg.SERIAL_NUM, "requirement": 0xFFFF},
1250            {"register": SMBusReg.MANUFACTURER_DATE, "requirement": None},
1251            {"register": SMBusReg.SPECIFICATION_INFO, "requirement": None},
1252            {"register": SMBusReg.DESIGN_VOLTAGE, "requirement": 16800},
1253            {
1254                "register": SMBusReg.DESIGN_CAPACITY,
1255                "requirement": math.floor(0.975 * CELL_CAPACITY_AH * 1000),
1256            },
1257            {"register": SMBusReg.CHARGING_VOLTAGE, "requirement": 16800},
1258            {"register": SMBusReg.CHARGING_CURRENT, "requirement": 2000},
1259            {"register": SMBusReg.MANUFACTURING_ACCESS, "requirement": 0},
1260            {"register": SMBusReg.REMAINING_TIME_ALARM, "requirement": 10},
1261            {"register": SMBusReg.REMAINING_CAPACITY, "requirement": 48},
1262            {"register": SMBusReg.AT_RATE_OK, "requirement": True},
1263            {"register": SMBusReg.AT_RATE_TIME_TO_EMPTY, "requirement": 65535},
1264            {"register": SMBusReg.AT_RATE_TIME_TO_FULL, "requirement": 65535},
1265            {"register": SMBusReg.AT_RATE, "requirement": 0},
1266            {"register": SMBusReg.BATTERY_MODE, "requirement": 0},
1267            {"register": SMBusReg.MAX_ERROR, "requirement": 0},
1268            {
1269                "register": SMBusReg.FULL_CHARGE_CAPACITY,
1270                "requirement": CELL_CAPACITY_AH * 1000,
1271            },
1272        ]
1273        test_failed = False
1274
1275        for element in constant_elements:
1276            register = element["register"]
1277            requirement = element["requirement"]
1278
1279            read_response = _smbus.read_register(register)
1280
1281            if register == SMBusReg.SERIAL_NUM:
1282                if read_response[0] != requirement:
1283                    logger.write_result_to_html_report(
1284                        f"{register.fname} had unique value of {read_response[0]} ({read_response[1]})"
1285                    )
1286                else:
1287                    logger.write_result_to_html_report(
1288                        f'<font color="#990000">{register.fname} was {read_response[0]} ({read_response[1]}) '
1289                        f"not a unique value</font>"
1290                    )
1291                    test_failed = True
1292                continue
1293
1294            if register == SMBusReg.REMAINING_CAPACITY:
1295                design_capacity = _smbus.read_register(SMBusReg.DESIGN_CAPACITY)
1296                requirement = design_capacity[0] // 2
1297                if requirement - 3 <= read_response[0] <= requirement + 3:
1298                    logger.write_result_to_html_report(f"{register.fname} had expected value of {requirement}")
1299                else:
1300                    logger.write_result_to_html_report(
1301                        f'<font color="#990000">{register.fname} was {read_response[0]} ({read_response[1]}) '
1302                        f"not {requirement}</font>"
1303                    )
1304                    test_failed = True
1305
1306            elif isinstance(requirement, int):
1307                if read_response[0] == requirement or read_response[1] == requirement.to_bytes(2, byteorder="little"):
1308                    logger.write_result_to_html_report(f"{register.fname} had expected value of {requirement}")
1309                else:
1310                    logger.write_result_to_html_report(
1311                        f'<font color="#990000">{register.fname} was {read_response[0]} ({read_response[1]}) '
1312                        f"not {requirement}</font>"
1313                    )
1314                    test_failed = True
1315            elif read_response[0] == requirement:
1316                logger.write_result_to_html_report(f"{register.fname} had expected value of {requirement}")
1317            else:
1318                logger.write_result_to_html_report(
1319                    f'<font color="#990000">{register.fname} was {read_response[0]} ({read_response[1]}) '
1320                    f"not {requirement}</font>"
1321                )
1322                test_failed = True
1323
1324        if test_failed:
1325            pytest.fail()
1326
1327        logger.write_result_to_html_report("All SMBus Constant Values passed")
1328
1329
1330class TestVariableSMBusValues:
1331    """Test variable SMBus values"""
1332
1333    class TestCycleCount(CSVRecordEvent):
1334        """@private Compare SMBus Cycle Count to reported count"""
1335
1336        cycle_count = 0
1337        required_value = None
1338
1339        @classmethod
1340        def failed(cls) -> bool:
1341            """Check if test parameters were exceeded"""
1342            return bool(cls.cycle_count != cls.required_value)
1343
1344        @classmethod
1345        def verify(cls, row, _serial_data, _cell_data):
1346            """Set Cycle count value"""
1347            cls.cycle_count = row["Cycle Count"]
1348            if cls.required_value is None:
1349                cls.required_value = cls.cycle_count + 1
1350
1351        @classmethod
1352        def result(cls):
1353            """Detailed test result information"""
1354            return f"Cycle Count: {cls.cmp(cls.cycle_count, '==', cls.required_value or 0, form='d')}"
1355
1356    class TestCurrent(CSVRecordEvent):
1357        """@private Compare SMBus Current value to expected value"""
1358
1359        allowable_error = 0.015
1360        smbus_data = SimpleNamespace(current=0, terminal=0, error=0.0)
1361
1362        @classmethod
1363        def failed(cls) -> bool:
1364            """Check if test parameters were exceeded"""
1365            return bool(cls.smbus_data.error > cls.allowable_error)
1366
1367        @classmethod
1368        def verify(cls, row, _serial_data, _cell_data):
1369            """Set current and expected values"""
1370            row_data = SimpleNamespace(current=row["Current (mA)"], terminal=row["HITL Current (A)"] * 1000)
1371            row_data.error = abs((row_data.current - row_data.terminal) / row_data.terminal)
1372            if abs(row_data.current) > 100:
1373                cls.smbus_data = max(cls.smbus_data, row_data, key=lambda data: data.error)
1374
1375        @classmethod
1376        def result(cls):
1377            """Detailed test result information"""
1378            return (
1379                f"Current error: {cls.cmp(cls.smbus_data.error, '<=', cls.allowable_error)} "
1380                f"(SMBus Current: {cls.smbus_data.current} mA, HITL Terminal Current: {cls.smbus_data.terminal} mA)"
1381            )
1382
1383    class TestVoltage(CSVRecordEvent):
1384        """@private Compare HITL Terminal Voltage to reported SMBus voltage"""
1385
1386        allowable_error = 0.015
1387        smbus_data = SimpleNamespace(voltage=0, terminal=0, error=0.0)
1388
1389        @classmethod
1390        def failed(cls) -> bool:
1391            """Check if test parameters were exceeded"""
1392            return bool(cls.smbus_data.error > cls.allowable_error)
1393
1394        @classmethod
1395        def verify(cls, row, _serial_data, _cell_data):
1396            """Voltage within range"""
1397            row_data = SimpleNamespace(voltage=row["Voltage (mV)"], terminal=row["HITL Voltage (V)"] * 1000)
1398            row_data.error = abs((row_data.voltage - row_data.terminal) / row_data.terminal)
1399            cls.smbus_data = max(cls.smbus_data, row_data, key=lambda data: data.error)
1400
1401        @classmethod
1402        def result(cls):
1403            """Detailed test result information."""
1404            return (
1405                f"Voltage error: {cls.cmp(cls.smbus_data.error, '<=', cls.allowable_error)} "
1406                f"(SMBus Voltage: {cls.smbus_data.voltage} mV, HITL Terminal Voltage: {cls.smbus_data.terminal} mV)"
1407            )
1408
1409    class TestTemperature(CSVRecordEvent):
1410        """@private Compare average HITL THERM1 & THERM2 temperatures to reported SMBus Temperature"""
1411
1412        smbus_temperature = 0
1413        average_hitl_temperature = 0
1414        low_temperature_range = 0
1415        high_temperature_range = 0
1416
1417        @classmethod
1418        def failed(cls) -> bool:
1419            """Check if test parameters were exceeded"""
1420            return (cls.smbus_temperature <= cls.low_temperature_range) or (
1421                cls.smbus_temperature >= cls.high_temperature_range
1422            )
1423
1424        @classmethod
1425        def verify(cls, row, _serial_data, _cell_data):
1426            """Voltage within range"""
1427            cls.smbus_temperature = row["Temperature (dK)"] / 10 - 273
1428            cls.average_hitl_temperature = statistics.mean([_plateset.thermistor1, _plateset.thermistor2])
1429            cls.low_temperature_range = cls.average_hitl_temperature - 5
1430            cls.high_temperature_range = cls.average_hitl_temperature + 5
1431
1432        @classmethod
1433        def result(cls):
1434            """Detailed test result information."""
1435            return (
1436                f"Temperature Error: "
1437                f"{cls.cmp(cls.smbus_temperature, '>=', cls.low_temperature_range, '°C', '.2f')}, "
1438                f" {cls.cmp(cls.smbus_temperature, '<=', cls.high_temperature_range, '°C', '.2f')},"
1439                f"(SMBus Temperature: {cls.smbus_temperature:.2f} °C, "
1440                f"HITL THERM1 & THERM2 Average Temperature: {cls.average_hitl_temperature:.2f} °C)"
1441            )
1442
1443    class TestCellVoltage1(CSVRecordEvent):
1444        """@private Compare HITL Cell Voltage 1 to reported Cell Voltage1"""
1445
1446        allowable_error = 0.01
1447        max = SimpleNamespace(cell_voltage=0, hitl_cell_voltage=0, error=0.0)
1448
1449        @classmethod
1450        def failed(cls) -> bool:
1451            """Check if test parameters were exceeded"""
1452            return bool(cls.max.error > cls.allowable_error)
1453
1454        @classmethod
1455        def verify(cls, row, _serial_data, _cell_data):
1456            """Cell Voltage 1 within range"""
1457            row_data = SimpleNamespace(
1458                cell_voltage=row["Cell Voltage1 (mV)"], hitl_cell_voltage=(float(row["Cell Sim 1 Volts (V)"]) * 1000)
1459            )
1460            row_data.error = abs((row_data.cell_voltage - row_data.hitl_cell_voltage) / row_data.hitl_cell_voltage)
1461            cls.max = max(cls.max, row_data, key=lambda data: data.error)
1462
1463        @classmethod
1464        def result(cls):
1465            """Detailed test result information."""
1466            return (
1467                f"Cell Voltage1 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)}"
1468                f"(SMBus Cell Voltage1: {cls.max.cell_voltage:.2f} mV, HITL Cell Sim 1 Voltage: "
1469                f"{cls.max.hitl_cell_voltage:.2f} mV)"
1470            )
1471
1472    class TestCellVoltage2(CSVRecordEvent):
1473        """@private Compare HITL Cell Voltage 2 to reported Cell Voltage2"""
1474
1475        allowable_error = 0.01
1476        max = SimpleNamespace(cell_voltage=0, hitl_cell_voltage=0, error=0.0)
1477
1478        @classmethod
1479        def failed(cls) -> bool:
1480            """Check if test parameters were exceeded"""
1481            return bool(cls.max.error > cls.allowable_error)
1482
1483        @classmethod
1484        def verify(cls, row, _serial_data, _cell_data):
1485            """Cell Voltage 2 within range"""
1486            row_data = SimpleNamespace(
1487                cell_voltage=row["Cell Voltage2 (mV)"], hitl_cell_voltage=(float(row["Cell Sim 2 Volts (V)"]) * 1000)
1488            )
1489            row_data.error = abs((row_data.cell_voltage - row_data.hitl_cell_voltage) / row_data.hitl_cell_voltage)
1490            cls.max = max(cls.max, row_data, key=lambda data: data.error)
1491
1492        @classmethod
1493        def result(cls):
1494            """Detailed test result information."""
1495            return (
1496                f"Cell Voltage2 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)}"
1497                f"(SMBus Cell Voltage2: {cls.max.cell_voltage:.2f} mV, "
1498                f"HITL Cell Sim 2 Voltage: {cls.max.hitl_cell_voltage:.2f} mV)"
1499            )
1500
1501    class TestCellVoltage3(CSVRecordEvent):
1502        """@private Compare HITL Cell Voltage 3 to reported Cell Voltage3"""
1503
1504        allowable_error = 0.01
1505        max = SimpleNamespace(cell_voltage=0, hitl_cell_voltage=0, error=0.0)
1506
1507        @classmethod
1508        def failed(cls) -> bool:
1509            """Check if test parameters were exceeded"""
1510            return bool(cls.max.error > cls.allowable_error)
1511
1512        @classmethod
1513        def verify(cls, row, _serial_data, _cell_data):
1514            """Cell Voltage 3 within range"""
1515            row_data = SimpleNamespace(
1516                cell_voltage=row["Cell Voltage3 (mV)"], hitl_cell_voltage=(float(row["Cell Sim 3 Volts (V)"]) * 1000)
1517            )
1518            row_data.error = abs((row_data.cell_voltage - row_data.hitl_cell_voltage) / row_data.hitl_cell_voltage)
1519            cls.max = max(cls.max, row_data, key=lambda data: data.error)
1520
1521        @classmethod
1522        def result(cls):
1523            """Detailed test result information."""
1524            return (
1525                f"Cell Voltage3 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)}"
1526                f"(SMBus Cell Voltage3: {cls.max.cell_voltage:.2f} mV, "
1527                f"HITL Cell Sim 3 Voltage: {cls.max.hitl_cell_voltage:.2f} mV)"
1528            )
1529
1530    class TestCellVoltage4(CSVRecordEvent):
1531        """@private Compare HITL Cell Voltage 4 to reported Cell Voltage4"""
1532
1533        allowable_error = 0.01
1534        max = SimpleNamespace(cell_voltage=0, hitl_cell_voltage=0, error=0.0)
1535
1536        @classmethod
1537        def failed(cls) -> bool:
1538            """Check if test parameters were exceeded"""
1539            return bool(cls.max.error > cls.allowable_error)
1540
1541        @classmethod
1542        def verify(cls, row, _serial_data, _cell_data):
1543            """Cell Voltage 4 within range"""
1544            row_data = SimpleNamespace(
1545                cell_voltage=row["Cell Voltage4 (mV)"], hitl_cell_voltage=(float(row["Cell Sim 4 Volts (V)"]) * 1000)
1546            )
1547            row_data.error = abs((row_data.cell_voltage - row_data.hitl_cell_voltage) / row_data.hitl_cell_voltage)
1548            cls.max = max(cls.max, row_data, key=lambda data: data.error)
1549
1550        @classmethod
1551        def result(cls):
1552            """Detailed test result information."""
1553            return (
1554                f"Cell Voltage4 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)}"
1555                f"(SMBus Cell Voltage4: {cls.max.cell_voltage:.2f} mV, "
1556                f"HITL Cell Sim 4 Voltage: {cls.max.hitl_cell_voltage:.2f} mV)"
1557            )
1558
1559    class TestStateOfCharge(CSVRecordEvent):
1560        """@private Compare HITL State of Charge to reported SMBus State of Charge"""
1561
1562        allowable_error = 5
1563        max = SimpleNamespace(absolute_charge=0, hitl_charge=0, error=0.0)
1564
1565        @classmethod
1566        def failed(cls) -> bool:
1567            """Check if test parameters were exceeded"""
1568            return bool(cls.max.error > cls.allowable_error)
1569
1570        @classmethod
1571        def verify(cls, row, _serial_data, _cell_data):
1572            """State of Charge within range"""
1573            row_data = SimpleNamespace(absolute_charge=row["Absolute State Of Charge (%)"])
1574            row_data.hitl_charge = min(
1575                float(row["Cell Sim 1 SOC (%)"].replace("%", "")),
1576                float(row["Cell Sim 2 SOC (%)"].replace("%", "")),
1577                float(row["Cell Sim 3 SOC (%)"].replace("%", "")),
1578                float(row["Cell Sim 4 SOC (%)"].replace("%", "")),
1579            )
1580            row_data.error = abs((row_data.absolute_charge - row_data.hitl_charge))
1581            cls.max = max(cls.max, row_data, key=lambda data: data.error)
1582
1583        @classmethod
1584        def result(cls):
1585            """Detailed test result information."""
1586            return (
1587                f"State of Charge error: {cls.cmp(cls.max.error, '<=', cls.allowable_error, '%', '.1f')}"
1588                f"(SMBus Absolute State of Charge: {cls.max.absolute_charge:.2f} %, "
1589                f"Lowest HITL State of Charge: {cls.max.hitl_charge:.2f} %)"
1590            )
1591
1592    def test_variable_smbus_values(self):
1593        """
1594           | Description          | Variable SMBus Values                                                  |
1595           | :------------------- | :--------------------------------------------------------------------- |
1596           | GitHub Issue         | turnaroundfactor/HITL#385                                       |
1597           | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1598jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D12) |
1599           | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
1600           | Instructions         | 1. Set thermistors to 23°C                                        </br>\
1601                                    2. Put cells in a rested state at 2.5V per cell                   </br>\
1602                                    3. Charge battery (16.8V / 3A / 100 mA cutoff)                    </br>\
1603                                    4. Rest for 3.1 hours (we want to calculate SoH change which requires 3 hours)</br>\
1604                                    5. Discharge at 2A until battery reaches 10V then stop discharging     |
1605           | Pass / Fail Criteria | Charging (mA) ----                                                </br>\
1606                                    ⦁ Expect Cycle Count [0x17] to be 0                               </br>\
1607                                    ⦁ Expect Current [0x0A] to be within 1% for abs(Terminal Current > 100mA) </br>\
1608                                    ⦁ Expect Voltage [0x09] to be HITL Terminal Voltage within 1%     </br>\
1609                                    ⦁ Expect Temperature [0x08] to be average of HITL THERM1 & THERM2 within 5°C </br>\
1610                                    ⦁ Expect Cell Voltage1 [0x3C] to be HITL Cell Sim 1 Volts (V)     </br>\
1611                                    ⦁ Expect Cell Voltage2 [0x3D] to be HITL Cell Sim 2 Volts (V)     </br>\
1612                                    ⦁ Expect Cell Voltage3 [0x3E] to be HITL Cell Sim 3 Volts (V)     </br>\
1613                                    ⦁ Expect Cell Voltage4 [0x3F] to be HITL Cell Sim 4 Volts (V)     </br>\
1614                                    ⦁ Expect State of Charge [0x4F] to be HITL State of Charge             |
1615           | Estimated Duration   | 18 minutes                                                             |
1616           | Note                 | When specified (see 3.1), batteries shall be compliant with System     \
1617                                    Management Bus (SMBus) Specification Revision 1.1 and Smart Battery    \
1618                                    Data (SBData) Specification, version 1.1, with the exception that      \
1619                                    SBData safety signal hardware requirements therein shall be replaced   \
1620                                    with a charge enable when a charge enable is specified (see 3.1 and    \
1621                                    3.5.6). Certification is required. Batteries shall be compatible       \
1622                                    with appropriate Level 2 and Level 3 chargers (see 6.4.7).             \
1623                                    When tested as specified in 4.7.2.15.1, SMBus data output shall be     \
1624                                    accurate within +0/-5% of the actual state of charge for the battery   \
1625                                    under test throughout the discharge. Manufacturer and battery data     \
1626                                    shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V     \
1627                                    logic circuitry. Pull-up resistors will be provided by the charger.    \
1628                                    SMBus circuitry shall respond to a SMBus query within 2 seconds.       \
1629                                    Smart batteries may act as master or slave on the bus, but must        \
1630                                    perform bus master timing arbitration according to the SMBus           \
1631                                   specification when acting as master.                                    |
1632        """
1633
1634        standard_charge()
1635        standard_rest(seconds=30 if FAST_MODE else 3.1 * 3600)
1636        standard_discharge()
1637
1638        # Check results
1639        if CSVRecordEvent.failed():
1640            pytest.fail(CSVRecordEvent.result())
1641
1642
1643@pytest.mark.parametrize("reset_test_environment", [{"volts": 4}], indirect=True)
1644class TestChargeAcceptance(CSVRecordEvent):
1645    """Run a test for charge acceptance"""
1646
1647    def test_charge_acceptance(self):
1648        """
1649        | Description          | Charge Acceptance                                                      |
1650        | :------------------- | :--------------------------------------------------------------------- |
1651        | GitHub Issue         | turnaroundfactor/HITL#517                                       |
1652        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1653jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D29) |
1654        | MIL-PRF Sections     | 3.5.10.1 (Charge Acceptance)                                           |
1655        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
1656                                 2. Put cells in rested state at 4.2V per cell                     </br>\
1657                                 3. Set THERM1 and THERM2 to -20°C                                 </br>\
1658                                 4. Attempt to charge at 1A                                        </br>\
1659                                 5. Attempt to discharge at 1A                                     </br>\
1660                                 6. Set THERM1 and THERM2 to 50°C                                  </br>\
1661                                 7. Attempt to charge at 1A                                        </br>\
1662                                 8. Attempt to discharge at 1A                                          |
1663        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be -20°C +/- 1.1°C            </br>\
1664                                 ⦁ Expect HITL Terminal Current to be 1A +/- 30mA                  </br>\
1665                                 ⦁ Expect HITL Terminal Current to be -1A +/- 30mA                 </br>\
1666                                 ⦁ Expect Serial THERM1 & THERM 2 to be 50°C +/- 1.1°C             </br>\
1667                                 ⦁ Expect HITL Terminal Current to be 1A +/- 30mA                  </br>\
1668                                 ⦁ Expect HITL Terminal Current to be -1A +/- 30mA                      |
1669        | Estimated Duration   | 17 seconds                                                              |
1670        | Note                 | When tested as specified in 4.7.2.9, batteries shall meet the          \
1671                                 capacity requirements 3.5.3 and the visual mechanical requirements     \
1672                                 of TABLE XIV. The surface temperature of the battery shall not         \
1673                                 exceed 185°F (85°C)                                                    |
1674        """
1675
1676        failed_tests = []
1677        temperatures = [-20, 50]
1678
1679        for set_temp in temperatures:
1680            logger.write_info_to_report(f"Setting THERM1 & THERM2 to {set_temp}°C")
1681
1682            _plateset.disengage_safety_protocols = True
1683            _plateset.thermistor1 = _plateset.thermistor2 = set_temp
1684            _plateset.disengage_safety_protocols = False
1685
1686            time.sleep(2)
1687
1688            # Get the serial data
1689            serial_data = serial_monitor.read()
1690
1691            # Convert temperature to Celsius from Kelvin
1692            therm_one = serial_data["dk_temp"] / 10 - 273
1693            therm_two = serial_data["dk_temp1"] / 10 - 273
1694            temp_range = f"{set_temp}°C +/- 1.1°C"
1695            low_range = set_temp - 1.1
1696            high_range = set_temp + 1.1
1697
1698            if low_range <= therm_one <= high_range:
1699                logger.write_result_to_html_report(
1700                    f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
1701                )
1702            else:
1703                logger.write_result_to_html_report(
1704                    f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
1705                    f"of {temp_range}</font>"
1706                )
1707                failed_tests.append("THERM1")
1708
1709            if low_range <= therm_two <= high_range:
1710                logger.write_result_to_html_report(
1711                    f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
1712                )
1713            else:
1714                logger.write_result_to_html_report(
1715                    f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
1716                    f"expected range of {temp_range}</font>"
1717                )
1718                failed_tests.append("THERM2")
1719
1720            logger.write_info_to_report("Attempting to charge at 1A")
1721            limit = 0.03
1722            with _bms.charger(16.8, 1):
1723                time.sleep(1)
1724                charger_amps = _bms.charger.amps
1725                low_range = 1 - limit
1726                high_range = 1 + limit
1727                expected_current_range = f"1A +/- {limit}A"
1728                if low_range <= charger_amps <= high_range:
1729                    logger.write_result_to_html_report(
1730                        f"HITL Terminal Current was {charger_amps:.3f}A after charging, which was within the "
1731                        f"expected range of {expected_current_range}"
1732                    )
1733                else:
1734                    logger.write_result_to_html_report(
1735                        f'<font color="#990000">HITL Terminal Current was {charger_amps:.3f}A after charging, '
1736                        f"which was not within the expected range of {expected_current_range} </font>"
1737                    )
1738                    failed_tests.append("HITL Terminal Current")
1739
1740            logger.write_info_to_report("Attempting to discharge at 1A")
1741            with _bms.load(1):
1742                time.sleep(1)
1743                load_amps = -1 * _bms.load.amps
1744                low_range = -1 - limit
1745                high_range = -1 + limit
1746                expected_current_range = f"-1A +/- {limit}A"
1747                if low_range <= load_amps <= high_range:
1748                    logger.write_result_to_html_report(
1749                        f"HITL Terminal Current was {load_amps:.3f}A after discharging, which was within the "
1750                        f"expected range of {expected_current_range}"
1751                    )
1752                else:
1753                    logger.write_result_to_html_report(
1754                        f'<font color="#990000">HITL Terminal Current was {load_amps:.3f}A after discharging, '
1755                        f"which was not within the expected range of {expected_current_range} </font>"
1756                    )
1757                    failed_tests.append("HITL Terminal Current")
1758
1759        if len(failed_tests) > 0:
1760            pytest.fail()
1761
1762        logger.write_result_to_html_report("All checks passed test")
1763
1764
1765@pytest.mark.parametrize("reset_test_environment", [{"volts": 4.2}], indirect=True)
1766class TestHighTemperaturePermanentCutoff(CSVRecordEvent):
1767    """Run a test for High temperature permanent cutoff"""
1768
1769    def test_high_temp_perm_cutoff(self):
1770        """
1771        | Description          | High Temperature Permanent Cutoff                                      |
1772        | :------------------- | :--------------------------------------------------------------------- |
1773        | GitHub Issue         | turnaroundfactor/HITL#516                                       |
1774        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1775jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D28) |
1776        | MIL-PRF Sections     | 3.7.2.5 (High Temperature permanent cut off devices)                   |
1777        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
1778                                 2. Put cells in rested state at 4.2V per cell                     </br>\
1779                                 3. Set THERM1 and THERM2 to 95°C                                  </br>\
1780                                 4. Measure Voltage                                                </br>\
1781                                 5. Attempt to charge at 1A                                        </br>\
1782                                 6. Attempt to discharge at 1A                                     </br>\
1783                                 7. Set temperature to 40°C                                        </br>\
1784                                 8. Measure Voltage                                                     |
1785        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 95°C +/- 5°C               </br>\
1786                                 ⦁ Expect Overtemperature Permanent Disable flag to be set         </br>\
1787                                 ⦁ HITL Terminal Voltage is 0V +/- 0.2 V                           </br>\
1788                                 ⦁ HITL Terminal Current is 0A +/- 1mA after charging            </br>\
1789                                 ⦁ HITL Terminal Current is 0A +/- 1mA after discharging         </br>\
1790                                 ⦁ HITL Terminal Voltage is 0V +/- 0.2 V                                |
1791        | Estimated Duration   | 6 seconds                                                              |
1792        | Note                 | The following test shall be performed. Charge batteries as specified   \
1793                                 in 4.6; use of 4.6.3 is permitted. Each battery shall be permanently   \
1794                                 shut off when the temperature of the battery reaches 199 ± 9°F         \
1795                                 (93 ± 5°C). The device shall prevent charging and discharging of the   \
1796                                 battery. The minimum quantity shall be as specified in the applicable  \
1797                                 specification sheet. When tested as specified in 4.7.4.10, battery     \
1798                                 voltage shall be zero volts after high temperature storage and shall   \
1799                                 remain at zero after 104°F (40°C) storage.                             |
1800        """
1801
1802        failed_tests = []
1803        timeout_seconds = 30
1804
1805        logger.write_info_to_report("Setting THERM1 & THERM2 to 95°C")
1806        _plateset.disengage_safety_protocols = True
1807        _plateset.thermistor1 = _plateset.thermistor2 = 95
1808        _plateset.disengage_safety_protocols = False
1809
1810        time.sleep(1)
1811        serial_data = serial_monitor.read()  # Get the serial data
1812        # Convert temperature to Celsius from Kelvin
1813        therm_one = serial_data["dk_temp"] / 10 - 273
1814        therm_two = serial_data["dk_temp1"] / 10 - 273
1815
1816        if 90 <= therm_one <= 100:
1817            logger.write_result_to_html_report(
1818                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of 95°C +/- 5°C"
1819            )
1820        else:
1821            logger.write_result_to_html_report(
1822                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
1823                f"of 95°C +/- 5°C</font>"
1824            )
1825            failed_tests.append("THERM1")
1826
1827        if 90 <= therm_two <= 100:
1828            logger.write_result_to_html_report(
1829                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of 95°C +/- 5°C"
1830            )
1831        else:
1832            logger.write_result_to_html_report(
1833                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
1834                f"expected range of 95°C +/- 5°C</font>"
1835            )
1836            failed_tests.append("THERM2")
1837
1838        start = time.perf_counter()
1839        while (serial_data := serial_monitor.read()) and not serial_data["flags.permanentdisable_overtemp"]:
1840            if time.perf_counter() - start > timeout_seconds:
1841                message = f"Over-temperature permanent disable was not raised after {timeout_seconds} seconds."
1842                logger.write_failure_to_html_report(message)
1843                failed_tests.append("Over-temperature permanent disable flag.")
1844                break
1845        else:
1846            logger.write_result_to_html_report("Over-temperature permanent disable was properly set.")
1847
1848        logger.write_info_to_report("Measuring voltage...")
1849        time.sleep(1)
1850
1851        volt_range = "0V (-0.2V / +3V)"
1852
1853        with _bms.load(0.0, DischargeType.CONSTANT_RESISTANCE, 50_000):
1854            if not -0.2 <= _bms.dmm.volts <= 3:
1855                logger.write_failure_to_html_report(
1856                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
1857                    f"which was not within the expected range of {volt_range}"
1858                )
1859                failed_tests.append("HITL Terminal Voltage after temperature was set to 95°C")
1860            else:
1861                logger.write_result_to_html_report(
1862                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
1863                    f"which was within the expected range of {volt_range}"
1864                )
1865
1866        logger.write_info_to_report("Attempting to charge at 1A")
1867        with _bms.charger(16.8, 1.0):
1868            time.sleep(1)
1869            if -0.020 <= _bms.charger.amps <= 0.020:
1870                logger.write_result_to_html_report(
1871                    f"HITL Terminal Current was {_bms.charger.amps:.3f}A, which was within the "
1872                    f"expected range of 0A +/- 20mA"
1873                )
1874            else:
1875                logger.write_result_to_html_report(
1876                    f'<font color="#990000">HITL Terminal Current was {_bms.charger.amps:.3f}A, which was'
1877                    f" not within the expected range of 0A +/- 20mA </font>"
1878                )
1879                failed_tests.append("HITL Terminal Current after attempting to charge at 1A")
1880
1881        logger.write_info_to_report("Attempting to discharge at 1A")
1882
1883        with _bms.load(1):
1884            time.sleep(1)
1885            if -0.020 <= _bms.load.amps <= 0.020:
1886                logger.write_result_to_html_report(
1887                    f"HITL Terminal Current was {_bms.load.amps:.3f}A, which was within the "
1888                    f"expected range of 0A +/- 20mA"
1889                )
1890            else:
1891                logger.write_result_to_html_report(
1892                    f'<font color="#990000">HITL Terminal Current was {_bms.load.amps:.3f}A, '
1893                    f"which was not within the expected range of 0A +/- 20mA </font>"
1894                )
1895                failed_tests.append("HITL Terminal Current after attempting to discharge at 1A")
1896
1897        logger.write_info_to_report("Setting Temperature to 40°C")
1898        _plateset.thermistor1 = _plateset.thermistor2 = 40
1899
1900        logger.write_info_to_report("Measuring voltage...")
1901        time.sleep(1)
1902        with _bms.load(0.0, DischargeType.CONSTANT_RESISTANCE, 50_000):
1903            if not -0.2 <= _bms.dmm.volts <= 3:
1904                logger.write_failure_to_html_report(
1905                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
1906                    f"which was not within the expected range of {volt_range}"
1907                )
1908                failed_tests.append("HITL Terminal Voltage after temperature was set to 40°C")
1909            else:
1910                logger.write_result_to_html_report(
1911                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V,"
1912                    f"which was within the expected range of {volt_range}"
1913                )
1914
1915        if len(failed_tests) > 0:
1916            message = f"Overall, the following checks failed: {', '.join(failed_tests)}"
1917            logger.write_result_to_html_report(f'<font color="#990000">{message}</font>')
1918            pytest.fail(message)
1919
1920        logger.write_result_to_html_report("All checks passed test")
1921
1922
1923@pytest.mark.parametrize("reset_test_environment", [{"volts": 4}], indirect=True)
1924class TestHighTemperatureTemporaryCutoff(CSVRecordEvent):
1925    """Run a test for High temperature temporary cutoff"""
1926
1927    def test_high_temperature_temp_cutoff(self):
1928        """
1929        | Description          | High Temperature Temporary Cutoff                                      |
1930        | :------------------- | :--------------------------------------------------------------------- |
1931        | GitHub Issue         | turnaroundfactor/HITL#517                                       |
1932        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1933jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D27) |
1934        | MIL-PRF Sections     | 3.7.2.4 (High Temperature permanent cut off devices)                   |
1935        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
1936                                 2. Put cells in rested state at 4.2V per cell                     </br>\
1937                                 3. Set THERM1 and THERM2 to 75°C                                  </br>\
1938                                 4. Measure Voltage                                                </br>\
1939                                 5. Attempt to charge at 1A                                        </br>\
1940                                 6. Attempt to discharge at 1A                                     </br>\
1941                                 7. Set temperature to 20°C                                        </br>\
1942                                 8. Measure Voltage                                                     |
1943        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 75°C +/- 5°C               </br>\
1944                                 ⦁ Expect Overtemp charge & Overtemp discharge flags to be set     </br>\
1945                                 ⦁ HITL Terminal Voltage is 0V -0.2, +3.0 V                        </br>\
1946                                 ⦁ HITL Terminal Current is 0A +/- 1mA after charging            </br>\
1947                                 ⦁ HITL Terminal Current is 0A +/- 1mA after discharging         </br>\
1948                                 ⦁ HITL Terminal Voltage is 0V -0.2, +3.0 V                             |
1949        | Estimated Duration   | 12 seconds                                                             |
1950        | Note                 | Each battery shall contain a minimum quantity of normally closed       \
1951                                 thermoswitches that shall open at 158 ± 9°F (70 ± 5°C) and close at    \
1952                                 122 ± 9°F (50 ± 5°C). Each thermoswitch shall make physical contact    \
1953                                 with not less than one cell. The minimum quantity shall be as          \
1954                                 specified (see 3.1). The quantity of thermoswitches shall be           \
1955                                 certified. When tested as specified in 4.7.4.9, battery voltage shall  \
1956                                 be zero volts after each high temperature storage and batteries shall  \
1957                                 meet the voltage requirement of 3.5.2 after cooling. After completion  \
1958                                 of the test the battery shall be able to meet the full discharge       \
1959                                 capacity requirement of 3.5.3 after full charge.                       |
1960        """
1961
1962        failed_tests = []
1963        timeout_seconds = 30
1964
1965        temperature = 75
1966        high_range = temperature + 5
1967        low_range = temperature - 5
1968        logger.write_info_to_report(f"Setting THERM1 & THERM2 to {temperature}°C")
1969        _plateset.thermistor1 = _plateset.thermistor2 = temperature
1970
1971        time.sleep(1)
1972        serial_data = serial_monitor.read()  # Get the serial data
1973        # Convert temperature to Celsius from Kelvin
1974        therm_one = serial_data["dk_temp"] / 10 - 273
1975        therm_two = serial_data["dk_temp1"] / 10 - 273
1976        temp_range_text = f"{temperature}°C +/- 5°C"
1977
1978        if low_range <= therm_one <= high_range:
1979            logger.write_result_to_html_report(
1980                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range_text}"
1981            )
1982        else:
1983            logger.write_result_to_html_report(
1984                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
1985                f"of {temp_range_text}</font>"
1986            )
1987            failed_tests.append("THERM1")
1988
1989        if low_range <= therm_two <= high_range:
1990            logger.write_result_to_html_report(
1991                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range_text}"
1992            )
1993        else:
1994            logger.write_result_to_html_report(
1995                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
1996                f"expected range of {temp_range_text}</font>"
1997            )
1998            failed_tests.append("THERM2")
1999
2000        start = time.perf_counter()
2001        while (serial_data := serial_monitor.read()) and not serial_data["flags.fault_overtemp_discharge"]:
2002            if time.perf_counter() - start > timeout_seconds:
2003                message = f"Over-temperature discharge flag was not raised after {timeout_seconds} seconds."
2004                logger.write_failure_to_html_report(message)
2005                failed_tests.append("Over-temperature discharge flag.")
2006                break
2007        else:
2008            logger.write_result_to_html_report("Over-temperature discharge flag was properly set.")
2009
2010        logger.write_info_to_report("Measuring voltage...")
2011        time.sleep(1)
2012
2013        low_range = -0.2
2014        high_range = 3
2015        volt_range_text = f"0V ({low_range}V / +{high_range}V)"
2016
2017        with _bms.load(0.0, DischargeType.CONSTANT_RESISTANCE, 50_000):
2018            if not -0.2 <= _bms.dmm.volts <= 3:
2019                logger.write_failure_to_html_report(
2020                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2021                    f"which was not within the expected range of {volt_range_text}"
2022                )
2023                failed_tests.append("HITL Terminal Voltage")
2024            else:
2025                logger.write_result_to_html_report(
2026                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2027                    f"which was within the expected range of {volt_range_text}"
2028                )
2029
2030        logger.write_info_to_report("Attempting to charge at 1A")
2031        high_range = 0.020
2032        amp_range_text = "0A +/- 20mA"
2033        with _bms.charger(16.8, 1.0):
2034            time.sleep(1)
2035            if (high_range * -1) <= _bms.charger.amps <= high_range:
2036                logger.write_result_to_html_report(
2037                    f"HITL Terminal Current was {_bms.charger.amps:.3f}A when charging, which was within the "
2038                    f"expected range of {amp_range_text}"
2039                )
2040            else:
2041                logger.write_result_to_html_report(
2042                    f'<font color="#990000">HITL Terminal Current was {_bms.charger.amps:.3f}A when charging, '
2043                    f"which was not within the expected range of {amp_range_text} </font>"
2044                )
2045                failed_tests.append("HITL Terminal Current after attempting to charge at 1A")
2046
2047        logger.write_info_to_report("Attempting to discharge at 1A")
2048
2049        with _bms.load(1):
2050            time.sleep(1)
2051            if (high_range * -1) <= _bms.load.amps <= high_range:
2052                logger.write_result_to_html_report(
2053                    f"HITL Terminal Current was {_bms.load.amps:.3f}A when discharging, which was within the "
2054                    f"expected range of {amp_range_text}"
2055                )
2056            else:
2057                logger.write_result_to_html_report(
2058                    f'<font color="#990000">HITL Terminal Current was {_bms.load.amps:.3f}A when discharging, '
2059                    f"which was not within the expected range of {amp_range_text} </font>"
2060                )
2061                failed_tests.append("HITL Terminal Current after attempting to discharge at 1A")
2062
2063        temperature = 20
2064        logger.write_info_to_report(f"Setting Temperature to {temperature}°C")
2065        _plateset.thermistor1 = _plateset.thermistor2 = temperature
2066
2067        logger.write_info_to_report("Measuring voltage...")
2068        time.sleep(1)
2069        high_range = 17
2070        low_range = 9
2071        if not (_plateset.load_switch or _plateset.charger_switch):
2072            with _bms.load(0.0, DischargeType.CONSTANT_RESISTANCE, 50_000):
2073                if not low_range <= _bms.dmm.volts <= high_range:
2074                    logger.write_failure_to_html_report(
2075                        f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2076                        f"which was not within the expected range of {low_range}V to {high_range}V"
2077                    )
2078                    failed_tests.append("HITL Terminal Voltage")
2079                else:
2080                    logger.write_result_to_html_report(
2081                        f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2082                        f"which was within expected range of {low_range}V to {high_range}V"
2083                    )
2084        else:
2085            if not low_range <= _bms.dmm.volts <= high_range:
2086                logger.write_failure_to_html_report(
2087                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, which was not within the "
2088                    f"expected range of {low_range}V to {high_range}V"
2089                )
2090                failed_tests.append("HITL Terminal Voltage")
2091            else:
2092                logger.write_result_to_html_report(
2093                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2094                    f"which was within expected range of {low_range}V to {high_range}V"
2095                )
2096
2097        if len(failed_tests) > 0:
2098            pytest.fail()
2099
2100        logger.write_result_to_html_report("All checks passed test")
2101
2102
2103@pytest.mark.parametrize("reset_test_environment", [{"volts": 4}], indirect=True)
2104class TestExtremeLowTempDischarge(CSVRecordEvent):
2105    """Run a test for extreme low temperature discharge"""
2106
2107    def test_extreme_low_temp_discharge(self):
2108        """
2109        | Description          | Extreme low temperature discharge                                      |
2110        | :------------------- | :--------------------------------------------------------------------- |
2111        | GitHub Issue         | turnaroundfactor/HITL#518                                       |
2112        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2113jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D28) |
2114        | MIL-PRF Sections     | 4.7.3.2 (Extreme low temperature discharge)                            |
2115        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
2116                                 2. Put cells in rested state at 4.2V per cell                     </br>\
2117                                 3. Set THERM1 and THERM2 to -30°C                                 </br>\
2118                                 4. Attempt to discharge at 1A                                          |
2119        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be -30°C +/- 1.1°C            </br>\
2120                                 ⦁ Expect HITL Terminal Current to be -1A +/- 30mA                      |
2121        | Estimated Duration   | 6 seconds                                                              |
2122        | Note                 | For Type III (Li-ion) batteries the following test shall be performed. \
2123                                 Charge batteries in accordance with 4.6; use of 4.6.3 is not permitted.\
2124                                 Store the batteries at -22 ± 2°F (-30 ± 1.1°C) for a minimum of        \
2125                                 4 hours. Discharge under these conditions at the rate specified to the \
2126                                 specified cutoff voltage (see 3.1). After testing, batteries shall     \
2127                                 meet the requirements of 3.5.3, 3.6, and 3.6.1.                        |
2128        """
2129
2130        failed_tests = []
2131
2132        logger.write_info_to_report("Setting THERM1 & THERM2 to -30°C")
2133        _plateset.disengage_safety_protocols = True
2134        _plateset.thermistor1 = _plateset.thermistor2 = -30
2135        _plateset.disengage_safety_protocols = False
2136
2137        # Get the serial data
2138        serial_data = serial_monitor.read()
2139
2140        # Convert temperature to Celsius from Kelvin
2141        therm_one = serial_data["dk_temp"] / 10 - 273
2142        therm_two = serial_data["dk_temp1"] / 10 - 273
2143        temp_range = "-30°C +/- 1.1°C"
2144
2145        if -31.1 <= therm_one <= -28.9:
2146            logger.write_result_to_html_report(
2147                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
2148            )
2149        else:
2150            logger.write_result_to_html_report(
2151                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
2152                f"of {temp_range}</font>"
2153            )
2154            failed_tests.append("THERM1 after setting temperature to -30°C")
2155
2156        if -31.1 <= therm_two <= -28.9:
2157            logger.write_result_to_html_report(
2158                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
2159            )
2160        else:
2161            logger.write_result_to_html_report(
2162                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
2163                f"expected range of {temp_range}</font>"
2164            )
2165            failed_tests.append("THERM2 after setting temperature to -30°C")
2166
2167        logger.write_info_to_report("Attempting to discharge at 1A")
2168
2169        with _bms.load(1):
2170            time.sleep(1)
2171            load_amps = -1 * _bms.load.amps
2172            expected_current_range = "-1A +/- 30mA"
2173            if -1.03 <= load_amps <= -0.97:
2174                logger.write_result_to_html_report(
2175                    f"HITL Terminal Current was {load_amps:.3f}A, which was within the "
2176                    f"expected range of {expected_current_range}"
2177                )
2178            else:
2179                logger.write_result_to_html_report(
2180                    f'<font color="#990000">HITL Terminal Current was {load_amps:.3f}A, '
2181                    f"which was not within the expected range of {expected_current_range} </font>"
2182                )
2183                failed_tests.append("HITL Terminal Current after attempting to discharge at 1A")
2184
2185        if len(failed_tests) > 0:
2186            message = f"Overall, the following checks failed: {', '.join(failed_tests)}"
2187            logger.write_result_to_html_report(f'<font color="#990000">{message}</font>')
2188            pytest.fail(message)
2189
2190        logger.write_result_to_html_report("All checks passed test")
2191
2192
2193@pytest.mark.parametrize("reset_test_environment", [{"volts": 4}], indirect=True)
2194class TestExtremeHighTempDischarge(CSVRecordEvent):
2195    """Run a test for extreme high temperature discharge"""
2196
2197    def test_extreme_high_temp_discharge(self):
2198        """
2199        | Description          | Extreme high temperature discharge                                     |
2200        | :------------------- | :--------------------------------------------------------------------- |
2201        | GitHub Issue         | turnaroundfactor/HITL#519                                       |
2202        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2203jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D31) |
2204        | MIL-PRF Sections     | 4.7.3.3 (Extreme high temperature discharge)                           |
2205        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
2206                                 2. Put cells in rested state at 4.2V per cell                     </br>\
2207                                 3. Set THERM1 and THERM2 to 55°C                                  </br>\
2208                                 4. Attempt to discharge at 1A                                          |
2209        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 55°C +/- 1.1°C             </br>\
2210                                 ⦁ Expect HITL Terminal Current to be -1A +/- 30mA                      |
2211        | Estimated Duration   | 6 seconds                                                              |
2212        | Note                 | For Type III (Li-ion) batteries the following test shall be performed. \
2213                                 Charge batteries in accordance with 4.6; use of 4.6.3 is not           \
2214                                 permitted. Store the batteries at 131 ± 2°F (55 ± 1.1°C) for a         \
2215                                 minimum of 4 hours. Discharge under these conditions at the rate       \
2216                                 specified to the specified cutoff voltage (see 3.1). After testing,    \
2217                                 batteries shall meet the requirements of 3.5.3, 3.6, and 3.6.1.        |
2218        """
2219
2220        failed_tests = []
2221        set_temp = 55
2222        logger.write_info_to_report(f"Setting THERM1 & THERM2 to {set_temp}°C")
2223        _plateset.disengage_safety_protocols = True
2224        _plateset.thermistor1 = _plateset.thermistor2 = set_temp
2225        _plateset.disengage_safety_protocols = False
2226
2227        # Get the serial data
2228        serial_data = serial_monitor.read()
2229
2230        # Convert temperature to Celsius from Kelvin
2231        therm_one = serial_data["dk_temp"] / 10 - 273
2232        therm_two = serial_data["dk_temp1"] / 10 - 273
2233        temp_range = f"{set_temp}°C +/- 1.1°C"
2234        low_range = set_temp - 1.1
2235        high_range = set_temp + 1.1
2236
2237        if low_range <= therm_one <= high_range:
2238            logger.write_result_to_html_report(
2239                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
2240            )
2241        else:
2242            logger.write_result_to_html_report(
2243                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
2244                f"of {temp_range}</font>"
2245            )
2246            failed_tests.append(f"THERM1 after setting temperature to {set_temp}°C")
2247
2248        if low_range <= therm_two <= high_range:
2249            logger.write_result_to_html_report(
2250                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
2251            )
2252        else:
2253            logger.write_result_to_html_report(
2254                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
2255                f"expected range of {temp_range}</font>"
2256            )
2257            failed_tests.append(f"THERM2 after setting temperature to {set_temp}°C")
2258
2259        logger.write_info_to_report("Attempting to discharge at 1A")
2260
2261        with _bms.load(1):
2262            time.sleep(1)
2263            load_amps = -1 * _bms.load.amps
2264            low_range = -1 - 0.03
2265            high_range = -1 + 0.03
2266            expected_current_range = "-1A +/- 30mA"
2267            if low_range <= load_amps <= high_range:
2268                logger.write_result_to_html_report(
2269                    f"HITL Terminal Current was {load_amps:.3f}A, which was within the "
2270                    f"expected range of {expected_current_range}"
2271                )
2272            else:
2273                logger.write_result_to_html_report(
2274                    f'<font color="#990000">HITL Terminal Current was {load_amps:.3f}A, '
2275                    f"which was not within the expected range of {expected_current_range} </font>"
2276                )
2277                failed_tests.append("HITL Terminal Current after attempting to discharge at 1A")
2278
2279        if len(failed_tests) > 0:
2280            message = f"Overall, the following checks failed: {', '.join(failed_tests)}"
2281            logger.write_result_to_html_report(f'<font color="#990000">{message}</font>')
2282            pytest.fail(message)
2283
2284        logger.write_result_to_html_report("All checks passed test")
2285
2286
2287class TestStateTransitions:
2288    """Run a test for state transition"""
2289
2290    def test_fast_sample(self, serial_watcher: SerialWatcher):
2291        """
2292        | Description          | Enter and exit fast sample state                                       |
2293        | :------------------- | :--------------------------------------------------------------------- |
2294        | GitHub Issue         | turnaroundfactor/HITL#478                                       |
2295        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2296jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D10)                          |
2297        | MIL-PRF Sections     | 3.5.7 (Power consumption)                                              |
2298        | Instructions         | 1. Ensure that we are in slow sample (the default state)          </br>\
2299                                 2. Transition to fast sample by charging 50mA+                    </br>\
2300                                 3. Maintain 50mA+ and ensure we are still in fast sample after 5 minutes </br>
2301                                 4. Transition to slow sample by discharging less than -50mA       </br>\
2302                                 5. Transition back to fast sample by charging 50mA+               </br>\
2303                                 6. Transition to deep slumber by charging/discharging in the range -50mA to 50mA </br>
2304                                 7. Transition back to fast sample by charging 50mA+                    |
2305        | Pass / Fail Criteria | ⦁ Fail if we are unable to transition to the desired state             |
2306        | Estimated Duration   | 5 minutes                                                              |
2307        | Note                 | The power consumption for all electronics within the battery shall     \
2308                                 be less than 350 micro-ampere average, per battery or independent      \
2309                                 section where applicable, at 68 ± 5°F (20 ± 2.8°C) (See 4.7.2.13).     |
2310        """
2311        logger.write_info_to_report("Testing Fast Sample")
2312        assert _bms.load and _bms.charger  # Make sure hardware exists
2313
2314        logger.write_result_to_html_report("Slow sample")
2315        serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2316
2317        logger.write_result_to_html_report("Slow sample -> Fast sample")
2318        with _bms.charger(16.8, 0.200):
2319            serial_watcher.assert_true("BMS_State", BMSState.FAST_SAMPLE)
2320            logger.write_result_to_html_report("Fast sample after 5 minutes")
2321            time.sleep(5.5 * 60)
2322            serial_watcher.assert_true("BMS_State", BMSState.FAST_SAMPLE)
2323
2324        logger.write_result_to_html_report("Fast sample -> Slow sample")
2325        with _bms.load(0.200):
2326            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2327
2328        logger.write_result_to_html_report("Slow sample -> Fast sample")
2329        with _bms.charger(16.8, 0.200):
2330            serial_watcher.assert_true("BMS_State", BMSState.FAST_SAMPLE)
2331
2332        logger.write_result_to_html_report("Fast sample -> Deep slumber")
2333        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=15 * 60)
2334
2335    def test_slow_sample(self, serial_watcher: SerialWatcher):
2336        """
2337        | Description          | Enter and exit slow sample state                                       |
2338        | :------------------- | :--------------------------------------------------------------------- |
2339        | GitHub Issue         | turnaroundfactor/HITL#478                                       |
2340        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2341jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D10)                          |
2342        | MIL-PRF Sections     | 3.5.7 (Power consumption)                                              |
2343        | Instructions         | 1. Ensure that we are in slow sample (the default state)          </br>\
2344                                 2. Transition to deep slumber by charging/discharging in the range -46mA to 20mA </br>\
2345                                 3. Transition back to slow sample by discharging less than -50mA  </br>\
2346                                 4. Test deep slumber timer (discharge)                            </br>\
2347                                 5. Charge/discharge in the range -46mA to 20mA for 2 minutes      </br>\
2348                                 6. Discharge less than -50mA for 2 minutes                        </br>\
2349                                 7. Transition to deep slumber by charging/discharging in the range -46mA to </br>\
2350                                 20mA, ensuring it takes 5 minutes                                 </br>\
2351                                 8. Test deep slumber timer (charge)                               </br>\
2352                                 9. Charge/discharge in the range -46mA to 20mA for 2 minutes      </br>\
2353                                 10. Charge in the range 20mA to 50mA for 2 minutes (keeps the comparator on <\br>\
2354                                 without entering fast sample)                                     </br>\
2355                                 11. Transition to deep slumber by charging/discharging in the range -46mA to </br>\
2356                                 20mA, ensuring it takes 5 minutes                                      |
2357        | Pass / Fail Criteria | ⦁ Fail if we are unable to transition to the desired state             |
2358        | Estimated Duration   | 5 minutes                                                              |
2359        | Note                 | The power consumption for all electronics within the battery shall     \
2360                                 be less than 350 micro-ampere average, per battery or independent      \
2361                                 section where applicable, at 68 ± 5°F (20 ± 2.8°C) (See 4.7.2.13).     |
2362        """
2363        logger.write_info_to_report("Testing Slow Sample")
2364        assert _bms.load and _bms.charger  # Make sure hardware exists
2365
2366        logger.write_result_to_html_report("Slow sample")
2367        serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2368
2369        logger.write_result_to_html_report("Slow sample -> Deep slumber")
2370        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2371
2372        logger.write_result_to_html_report("Deep slumber -> Slow sample")
2373        with _bms.load(0.200):
2374            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2375
2376        logger.write_result_to_html_report("Slow sample -> Deep slumber after 5 minutes (discharging)")
2377        serial_watcher.assert_measurements()
2378        time.sleep(2 * 60)
2379        with _bms.load(0.200):
2380            time.sleep(2 * 60)
2381        wait_start_time = time.perf_counter()
2382        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2383        if state_events := serial_watcher.events.get("BMS_State"):
2384            logger.write_result_to_html_report(f"Receive time: {state_events[-1].time - wait_start_time:.6f} seconds")
2385            assert state_events[-1].time - wait_start_time >= 5 * 60
2386
2387        logger.write_result_to_html_report("Deep slumber -> Slow sample")
2388        with _bms.load(0.200):
2389            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2390
2391        logger.write_result_to_html_report("Slow sample -> Deep slumber after 5 minutes (charging)")
2392        serial_watcher.assert_measurements()
2393        time.sleep(2 * 60)
2394        with _bms.charger(16.8, 0.040):
2395            time.sleep(2 * 60)
2396        wait_start_time = time.perf_counter()
2397        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2398        if state_events := serial_watcher.events.get("BMS_State"):
2399            logger.write_result_to_html_report(f"Receive time: {state_events[-1].time - wait_start_time:.6f} seconds")
2400            assert state_events[-1].time - wait_start_time >= 5 * 60
2401
2402        logger.write_result_to_html_report("Slow sample state test passed")
2403
2404    def test_deep_slumber(self, serial_watcher: SerialWatcher):
2405        """
2406        | Description          | Enter and exit slow deep slumber state                                 |
2407        | :------------------- | :--------------------------------------------------------------------- |
2408        | GitHub Issue         | turnaroundfactor/HITL#478                                       |
2409        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2410jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D10)                          |
2411        | MIL-PRF Sections     | 3.5.7 (Power consumption)                                              |
2412        | Instructions         | 1. Ensure that we are in slow sample (the default state)          </br>\
2413                                 2. Transition to deep slumber by charging/discharging in the range -46mA to 20mA </br>\
2414                                 3. Transition back to slow sample by discharging less than -50mA  </br>\
2415                                 4. Transition to deep slumber by charging/discharging in the range -46mA to 20mA </br>\
2416                                 5. Transition back to slow sample by charging in the range 20mA to 40mA          </br>\
2417                                 6. Transition to deep slumber by charging/discharging in the range -46mA to 20mA </br>\
2418                                 7. Transition back to slow sample by setting charge enable low while resting |
2419        | Pass / Fail Criteria | ⦁ Fail if we are unable to transition to the desired state             |
2420        | Estimated Duration   | 5 minutes                                                              |
2421        | Note                 | The power consumption for all electronics within the battery shall     \
2422                                 be less than 350 micro-ampere average, per battery or independent      \
2423                                 section where applicable, at 68 ± 5°F (20 ± 2.8°C) (See 4.7.2.13).     |
2424        """
2425        logger.write_info_to_report("Testing Deep slumber")
2426        assert _bms.load and _bms.charger  # Make sure hardware exists
2427
2428        logger.write_result_to_html_report("Slow sample")
2429        serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE, wait_time=5 * 60)
2430
2431        logger.write_result_to_html_report("Slow sample -> Deep slumber")
2432        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2433
2434        logger.write_result_to_html_report("Deep slumber -> Slow sample (discharging)")
2435        with _bms.load(0.200):
2436            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2437
2438        logger.write_result_to_html_report("Slow sample -> Deep slumber")
2439        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2440
2441        logger.write_result_to_html_report("Deep slumber -> Slow sample (charging)")
2442        with _bms.charger(16.8, 0.040):
2443            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2444
2445        logger.write_result_to_html_report("Slow sample -> Deep slumber")
2446        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2447
2448        logger.write_result_to_html_report("Deep slumber -> Slow sample (CE pin)")
2449        _plateset.ce_switch = True
2450        try:
2451            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2452        finally:  # Ensure ce pin is reset regardless of outcome
2453            _plateset.ce_switch = False
2454
2455        logger.write_result_to_html_report("Deep slumber state test passed")
2456
2457
2458@pytest.mark.parametrize("reset_test_environment", [{"volts": 2.5}], indirect=True)
2459class TestVoltageAccuracy:
2460    """Run a test for voltage accuracy"""
2461
2462    class VoltageCellAccuracy(CSVRecordEvent):
2463        """@private Check if HITL cell sim voltage matches serial cell sim voltage within 1%"""
2464
2465        percent_closeness = 0.05
2466        max = SimpleNamespace(cell_id=0, sim_v=0, bms_v=0, error=0.0)
2467
2468        @classmethod
2469        def failed(cls) -> bool:
2470            """Check if test parameters were exceeded."""
2471            return bool(cls.max.error > cls.percent_closeness)
2472
2473        @classmethod
2474        def verify(cls, row, serial_data, _cell_data):
2475            """Cell voltage within range"""
2476            for i, cell_id in enumerate(_bms.cells):
2477                row_data = SimpleNamespace(
2478                    cell_id=cell_id,
2479                    sim_v=row[f"ADC Plate Cell {cell_id} Voltage (V)"],
2480                    bms_v=serial_data[f"mvolt_cell{'' if i == 0 else i}"] / 1000,
2481                )
2482                row_data.error = abs((row_data.bms_v - row_data.sim_v) / row_data.sim_v)
2483                cls.max = max(cls.max, row_data, key=lambda data: data.error)
2484
2485        @classmethod
2486        def result(cls):
2487            """Detailed test result information."""
2488            return (
2489                f"Cell Voltage Error: {cls.cmp(cls.max.error, '<=', cls.percent_closeness)}"
2490                f"(Sim {cls.max.cell_id}: {cls.max.sim_v * 1000:.1f} mv, BMS: {cls.max.bms_v * 1000:.1f} mv)"
2491            )
2492
2493    class TerminalVoltageAccuracy(CSVRecordEvent):
2494        """@private Compare HITL voltage to reported Terminal voltage."""
2495
2496        percent_closeness = 0.05
2497        max = SimpleNamespace(hitl_v=0, bms_v=0, error=0.0)
2498
2499        @classmethod
2500        def failed(cls) -> bool:
2501            """Check if test parameters were exceeded."""
2502            return bool(cls.max.error > cls.percent_closeness)
2503
2504        @classmethod
2505        def verify(cls, row, serial_data, _cell_data):
2506            """Terminal voltage within range"""
2507            row_data = SimpleNamespace(hitl_v=row["HITL Voltage (V)"], bms_v=serial_data["mvolt_terminal"] / 1000)
2508            row_data.error = abs((row_data.bms_v - row_data.hitl_v) / row_data.hitl_v)
2509            cls.max = max(cls.max, row_data, key=lambda data: data.error)
2510
2511        @classmethod
2512        def result(cls):
2513            """Detailed test result information."""
2514            return (
2515                f"Terminal Voltage error: {cls.cmp(cls.max.error, '<=', cls.percent_closeness)} "
2516                f"(HITL: {cls.max.hitl_v * 1000:.1f} mv, BMS: {cls.max.bms_v * 1000:.1f} mv)"
2517            )
2518
2519    def test_voltage_accuracy(self):
2520        """
2521        | Description          | Test the cell voltage accuracy                                         |
2522        | :------------------- | :--------------------------------------------------------------------- |
2523        | GitHub Issue         | turnaroundfactor/HITL#399                                       |
2524        | MIL-PRF Sections     | 3.5.8.3 (Accuracy)                                                </br>\
2525                                 4.7.2.14.3 (Accuracy During Discharge)                                 |
2526        | Instructions         | 1. Set thermistors to 23C                                         </br>\
2527                                 2. Put cells in a rested state at 2.5V per cell                   </br>\
2528                                 3. Charge battery (16.8V / 3A / 100 mA cutoff)                    </br>\
2529                                 4. Increase cell voltages in 100 mV increments up to and including 4.2V </br>\
2530        | Pass / Fail Criteria | ⦁ HITL cell sim voltage matches serial cell sim voltage to within 1%  </br>\
2531                                 ⦁ HITL voltage matches serial Terminal voltage to within 1%   </br>\
2532        | Estimated Duration   | 12 hours                                                               |
2533        | Note                 | MIL-PRF 3.5.8.3 (Accuracy): The values of the display shall be         \
2534                                 accurate within +0/-5% of the actual values for the battery.           \
2535                                 (see 4.7.2.14.3).                                                      |
2536        """
2537        _bms.timer.reset()  # Keep track of runtime
2538        for target_mv in range(2500, 4300, 100):
2539            voltages = ", ".join(f"{cell.volts}V" for cell in _bms.cells.values())
2540            logger.write_info_to_report(f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Voltage: {voltages}, ")
2541            _bms.csv.cycle.record(_bms.timer.elapsed_time)
2542            for cell in _bms.cells.values():
2543                cell.volts = target_mv / 1000
2544            time.sleep(3)
2545
2546        if CSVRecordEvent.failed():  # FIXME(JA): make this implicit?
2547            pytest.fail(CSVRecordEvent.result())
2548
2549
2550# @pytest.mark.parametrize("reset_test_environment", [{"volts": 2.5}], indirect=True)
2551@pytest.mark.parametrize("reset_test_environment", [{"volts": 3.5}], indirect=True)
2552class TestSerialFaults:
2553    """Test all faults"""
2554
2555    def test_wakeup_1(self, serial_watcher: SerialWatcher):
2556        """
2557        | Description          | Test Wakeup 1                                                          |
2558        | :------------------- | :--------------------------------------------------------------------- |
2559        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2560        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2561jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2562        | MIL-PRF Sections     | 3.6.8.2 (Battery wake up)                                              |
2563        | Instructions         | 1. Set Default state at 0mA                                       </br>\
2564                                 2. Discharge at 100 mA (below -46 mA)                             </br>\
2565                                 3. Discharge at 10 mA (between -46mA and 19ma)                    </br>\
2566                                 4. Charge at 100 mA, (above 19 mA)                                     |
2567        | Pass / Fail Criteria | ⦁ n_wakeup_gpio = 1, with default state at 0 mA                   </br>\
2568                                 ⦁ n_wakeup_gpio = 0, with default state at 100 mA                   </br>\
2569                                 ⦁ n_wakeup_gpio = 1, with default state at 10 mA                   </br>\
2570                                 ⦁ n_wakeup_gpio = 0, with default state at 0 mA                        |
2571        | Estimated Duration   | 10 seconds                                                             |
2572        | Note                 | The BMS should be in a state of slumber if the current measured is     \
2573                                 between -46mA and 19ma. This is done using internal comparators on the \
2574                                 board to a logical AND chip feeding into an interrupt pin. To test     \
2575                                 this, 3 different currents should be set. One current below -46ma,     \
2576                                 another current between -46mA and 19ma, and another current above      \
2577                                 19ma. If the current is within the allowable range, we should read     \
2578                                 logic 1 on the N_WAKEUP pin. If the current is outside (above or       \
2579                                 below) we should read logic 0 on the pin.                              |
2580        """
2581
2582        logger.write_info_to_report("Testing Wakeup")
2583        assert _bms.load and _bms.charger  # Confirm hardware is available
2584
2585        logger.write_result_to_html_report("Default state")
2586        serial_watcher.assert_true("n_wakeup_gpio", True, 1)
2587
2588        logger.write_result_to_html_report("Discharging 200 mA")
2589        with _bms.load(0.200):
2590            serial_watcher.assert_true("n_wakeup_gpio", False, 2)
2591
2592        logger.write_result_to_html_report("Discharging 10 mA")
2593        with _bms.load(0.010):
2594            serial_watcher.assert_true("n_wakeup_gpio", True, 3)
2595
2596        logger.write_result_to_html_report("Charging 200 mA")
2597        with _bms.charger(16.8, 0.200):
2598            serial_watcher.assert_true("n_wakeup_gpio", False, 4)
2599
2600    def test_overtemp_fault(self, serial_watcher: SerialWatcher):
2601        """
2602        | Description          | Test over temperature Fault                                             |
2603        | :------------------- | :--------------------------------------------------------------------- |
2604        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2605        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2606jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2607        | MIL-PRF Sections     | 4.7.3.3 (Extreme high temperature discharge)                           |
2608        | Instructions         | 1. Set Default state at 15                                        </br>\
2609                                 2. Resting at 59°C (at or above 59°C)                             </br>\
2610                                 3. Discharging at 100 mA, 59°C (at or above 59°C)                 </br>\
2611                                 4. Charging at 100 mA, 54°C (at or above 53°C)                    </br>\
2612                                 5. Charging at 100 mA, 94°C (at or above 93°C)                    </br>\
2613                                 6. Discharging at 100 mA, 94°C (at or above 93°C)                 </br>\
2614                                 7. Rest at 94°C (at or above 93°C)                                      |
2615        | Pass / Fail Criteria | ⦁ Prefault overtemp Discharge value is False (default state)       </br>\
2616                                 ⦁ Fault overtemp Discharge value is False (default state)          </br>\
2617                                 ⦁ Prefault overtemp charge value is False (default state)          </br>\
2618                                 ⦁ Fault overtemp charge value is False (default state)             </br>\
2619                                 ⦁ Permanent Disable overtemp is False (default state)              </br>\
2620                                 ⦁ Measure output fets disabled is  False (default state)           </br>\
2621                                 ⦁ Resting overtemp discharge is True for fault & prefault          </br>\
2622                                 ⦁ Discharging overtemp value is True for fault & prefault          </br>\
2623                                 ⦁ After charging permanent disable, permanent disable overtemp is True </br>\
2624                                 ⦁ After resting permanent disable, permanent disable overtemp is True  |
2625        | Estimated Duration   | 12 hours                                                               |
2626        | Note                 | If our batteries get too hot, we must trigger a fault. This is         \
2627                                 common during high discharge or charging cycles. There are 3 different \
2628                                 environments where we would trigger an overtemp fault: charge,         \
2629                                 discharge and resting. While charging, if we are above 53C we must     \
2630                                 trigger a fault If we are resting or discharging at 59 degrees we must \
2631                                 trigger a fault. Both of these faults should trigger a prefault        \
2632                                 condition in our flags. After we cycle again we should then trigger a  \
2633                                 full fault. If the temperature ever goes above 93 degrees, the fault   \
2634                                 should never clear and we should be in permanent fault and trigger     \
2635                                 the fets. (This should also be seen in the flags)                      |
2636        """
2637        logger.write_info_to_report("Testing Overtemp")
2638        assert _bms.load and _bms.charger  # Confirm hardware is available
2639
2640        # Test default state
2641        logger.write_result_to_html_report(f"Resting at 0 mA, {CELL_VOLTAGE} V, 15°C")
2642        _plateset.thermistor1 = 15
2643        serial_watcher.assert_true("flags.prefault_overtemp_discharge", False, 1)
2644        serial_watcher.assert_true("flags.fault_overtemp_discharge", False, 1)
2645        serial_watcher.assert_true("flags.prefault_overtemp_charge", False, 1)
2646        serial_watcher.assert_true("flags.fault_overtemp_charge", False, 1)
2647        serial_watcher.assert_true("flags.permanentdisable_overtemp", False, 1)
2648        serial_watcher.assert_true("flags.measure_output_fets_disabled", False, 1)
2649
2650        # Test resting overtemp
2651        logger.write_result_to_html_report("Resting at 59°C")
2652        _plateset.thermistor1 = 59
2653        serial_watcher.assert_true("flags.prefault_overtemp_discharge", True, 2)
2654        serial_watcher.assert_true("flags.fault_overtemp_discharge", True, 2)
2655        logger.write_result_to_html_report("Resting at 15°C")
2656        _plateset.thermistor1 = 15
2657        serial_watcher.assert_true("flags.prefault_overtemp_discharge", False, 3)
2658        serial_watcher.assert_true("flags.fault_overtemp_discharge", False, 3)
2659
2660        # Test discharging overtemp
2661        logger.write_result_to_html_report("Discharging at -100 mA, 59°C")
2662        with _bms.load(0.100):
2663            _plateset.thermistor1 = 59
2664            serial_watcher.assert_true("flags.prefault_overtemp_discharge", True, 4)
2665            serial_watcher.assert_true("flags.fault_overtemp_discharge", True, 4)
2666            logger.write_result_to_html_report("Discharging at -100 mA, 15°C")
2667            _plateset.thermistor1 = 15
2668            serial_watcher.assert_true("flags.prefault_overtemp_discharge", False, 5)
2669            serial_watcher.assert_true("flags.fault_overtemp_discharge", False, 5)
2670
2671        # Test charging overtemp
2672        logger.write_result_to_html_report("Charging at 100 mA, 54°C")
2673        with _bms.charger(16.8, 0.200):
2674            _plateset.thermistor1 = 54
2675            serial_watcher.assert_true("flags.prefault_overtemp_charge", True, 2)
2676            serial_watcher.assert_true("flags.fault_overtemp_charge", True, 2)
2677            logger.write_result_to_html_report("Charging at 100 mA, 15°C")
2678            _plateset.thermistor1 = 15
2679            serial_watcher.assert_true("flags.prefault_overtemp_charge", False, 3)
2680            serial_watcher.assert_true("flags.fault_overtemp_charge", False, 3)
2681
2682        # Test charging permanent disable
2683        logger.write_result_to_html_report("Charging at 100 mA, 94°C")
2684        _plateset.disengage_safety_protocols = True
2685        _plateset.thermistor1 = 94
2686        _plateset.disengage_safety_protocols = False
2687        with _bms.charger(16.8, 0.200):
2688            serial_watcher.assert_true("flags.permanentdisable_overtemp", True, 2)
2689            logger.write_result_to_html_report("Charging at 100 mA, 15°C")
2690            _plateset.thermistor1 = 15
2691            serial_watcher.assert_false("flags.permanentdisable_overtemp", False, 3)
2692
2693        # Test discharging permanent disable
2694        logger.write_result_to_html_report("Discharging at -100 mA, 94°C")
2695        _plateset.disengage_safety_protocols = True
2696        _plateset.thermistor1 = 94
2697        _plateset.disengage_safety_protocols = False
2698        with _bms.load(0.100):
2699            serial_watcher.assert_true("flags.permanentdisable_overtemp", True, 2)
2700            logger.write_result_to_html_report("Discharging at -100 mA, 15°C")
2701            _plateset.thermistor1 = 15
2702            serial_watcher.assert_false("flags.permanentdisable_overtemp", False, 3)
2703
2704        # Test resting permanent disable
2705        _plateset.disengage_safety_protocols = True
2706        logger.write_result_to_html_report("Resting at 94°C")
2707        _plateset.thermistor1 = 94
2708        _plateset.disengage_safety_protocols = False
2709        serial_watcher.assert_true("flags.permanentdisable_overtemp", True, 2)
2710        logger.write_result_to_html_report("Resting at 15°C")
2711        _plateset.thermistor1 = 15
2712        serial_watcher.assert_false("flags.permanentdisable_overtemp", False, 3)
2713
2714    def test_undertemp_faults(self, serial_watcher: SerialWatcher):
2715        """
2716        | Description          | Test under temperature Fault                                             |
2717        | :------------------- | :--------------------------------------------------------------------- |
2718        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2719        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2720jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2721        | MIL-PRF Sections     | 4.7.2.7 (Low Temperature Discharge)                                    |
2722        | Instructions         | 1. Set Default state at 15                                        </br>\
2723                                 2. Resting at -35°C (at or below -33.2°C)                         </br>\
2724                                 3. Discharging at -100 mA, -35°C (at or below -33.2°C)            </br>\
2725                                 4. Discharging at -100 mA, -30°C (at or above -32.2°C)            </br>\
2726                                 5. Charging at 100 mA, -25°C (at or below -23.2°C)                </br>\
2727                                 6. Charging at 100 mA, -20°C (at or above -22.2°C)                     |
2728        | Pass / Fail Criteria | ⦁ Prefault overtemp Discharge value is False (default state)       </br>\
2729                                 ⦁ Fault overtemp Discharge value is False (default state)          </br>\
2730                                 ⦁ Prefault overtemp charge value is False (default state)          </br>\
2731                                 ⦁ Fault overtemp charge value is False (default state)             </br>\
2732                                 ⦁ Permanent Disable overtemp is False (default state)              </br>\
2733                                 ⦁ Measure output fets disabled is  False (default state)           </br>\
2734                                 ⦁ Resting overtemp discharge is True for fault & prefault          </br>\
2735                                 ⦁ Discharging overtemp value is True for fault & prefault          </br>\
2736                                 ⦁ After charging permanent disable, permanent disable overtemp is True </br>\
2737                                 ⦁ After resting permanent disable, permanent disable overtemp is True  |
2738        | Estimated Duration   | 10 seconds                                                             |
2739        | Note                 | This occurs when we read more than 20 mamps from the battery, if any   \
2740                                 of the cells are under 0 degrees Celsius this will trigger a fault.    \
2741                                 This will be cleared if we go above -2C. Regardless of current being   \
2742                                 measured, if we ever read below -20C, this should trigger a fault.     \
2743                                 This fault should not be cleared until we are above -18C               |
2744        """
2745        logger.write_info_to_report("Testing Undertemp")
2746        assert _bms.load and _bms.charger  # Confirm hardware is available
2747
2748        logger.write_result_to_html_report(f"Resting at 0 mA, {CELL_VOLTAGE} V, 15°C")
2749        _plateset.thermistor1 = 15
2750
2751        # Discharging flags
2752        serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 1)
2753        serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 1)
2754
2755        # Charging flags
2756        serial_watcher.assert_true("flags.prefault_undertemp_charge", False, 1)
2757        serial_watcher.assert_true("flags.fault_undertemp_charge", False, 1)
2758
2759        logger.write_result_to_html_report("Resting at -35°C")
2760        _plateset.thermistor1 = -35
2761        serial_watcher.assert_true("flags.prefault_undertemp_discharge", True, 2)
2762        serial_watcher.assert_true("flags.fault_undertemp_discharge", True, 2)
2763        logger.write_result_to_html_report("Resting at -30°C")
2764        _plateset.thermistor1 = -30
2765        serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 3)
2766        serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 3)
2767
2768        logger.write_result_to_html_report("Discharging at -100 mA, -35°C")
2769        with _bms.load(0.100):
2770            _plateset.thermistor1 = -35
2771            serial_watcher.assert_true("flags.prefault_undertemp_discharge", True, 4)
2772            serial_watcher.assert_true("flags.fault_undertemp_discharge", True, 4)
2773            logger.write_result_to_html_report("Discharging at -100 mA, -30°C")
2774            _plateset.thermistor1 = -30
2775            serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 5)
2776            serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 5)
2777
2778        logger.write_result_to_html_report("Charging at 100 mA, -25°C")
2779        with _bms.charger(16.8, 0.200):
2780            _plateset.thermistor1 = -25
2781            serial_watcher.assert_true("flags.prefault_undertemp_charge", True, 2)
2782            serial_watcher.assert_true("flags.fault_undertemp_charge", True, 2)
2783            logger.write_result_to_html_report("Charging at 100 mA, 20°C")
2784            _plateset.thermistor1 = 20
2785            serial_watcher.assert_true("flags.prefault_undertemp_charge", False, 3)
2786            serial_watcher.assert_true("flags.fault_undertemp_charge", False, 3)
2787
2788    def set_exact_volts(self, cell: Cell, voltage: float, compensation: float = 0.08):
2789        """What the BMS reads won't exactly match the set voltage, thus we need slight adjustments."""
2790        cell.exact_volts = voltage + compensation
2791        logger.write_debug_to_report(f"Cell is {cell.volts}V")
2792
2793    def test_overvoltage_faults(self, serial_watcher: SerialWatcher):
2794        """
2795        | Description          | Test over voltage faults                                             |
2796        | :------------------- | :--------------------------------------------------------------------- |
2797        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2798        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2799jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2800        | MIL-PRF Sections     | 4.6.1 (Standard Charge)                                                |
2801        | Instructions         | 1. Rest at 0 mA, 3.8002 V                                         </br>\
2802                                 2. Charge at 100 mA, 4.21 V                                       </br>\
2803                                 3. Charge at 100 mA, 4.10 V                                       </br>\
2804                                 4. Charge at 100 mA, 4.26 V                                       </br>\
2805                                 5. Charge at 100 mA, 4.10 V                                            |
2806        | Pass / Fail Criteria | ⦁ Prefault over voltage charge is False at 3.8002 V               </br>\
2807                                 ⦁ Fault over voltage charge is False at 3.8002 V                  </br>\
2808                                 ⦁ Permanent disable over votlage charge is False at 3.8002 V      </br>\
2809                                 ⦁ Prefault over voltage charge is True at 4.21 V                  </br>\
2810                                 ⦁ Fault over voltage charge is True at 4.21 V                     </br>\
2811                                 ⦁ Prefault over voltage charge is False at 4.10 V                 </br>\
2812                                 ⦁ Fault over voltage charge is False at 4.10 V                    </br>\
2813                                 ⦁ Permanent disable over voltage is True at 4.26 V                </br>\
2814                                 ⦁ Permanent disable over voltage is False at 4.10 V                    |
2815        | Estimated Duration   | 10 seconds                                                             |
2816        | Note                 | While charging, we need to monitor the voltage of our cells.           \
2817                                 Specifically, if a cell ever goes above 4.205 Volts, we should         \
2818                                 trigger a prefault. If this prefault exsists for more than 3 seconds,  \
2819                                 we then should trigger a full fault. If a cell ever gets to be above   \
2820                                 4.250 volts, we should trigger a permanent fault. If we go under 4.201 \
2821                                 we should be able to clear the fault                                   |
2822        """
2823        logger.write_info_to_report("Testing Overvoltage")
2824        assert _bms.load and _bms.charger  # Confirm hardware is available
2825        test_cell = _bms.cells[1]
2826        for cell in _bms.cells.values():
2827            cell.disengage_safety_protocols = True
2828            # Must be high enough to not trigger cell imbalance [abs(high - low) > 0.5V]
2829            self.set_exact_volts(cell, 4.0, 0)
2830
2831        logger.write_result_to_html_report(f"Resting at 0 mA, {CELL_VOLTAGE} V")
2832        serial_watcher.assert_true("flags.prefault_overvoltage_charge", False, 1)
2833        serial_watcher.assert_true("flags.fault_overvoltage_charge", False, 1)
2834        serial_watcher.assert_true("flags.permanentdisable_overvoltage", False, 1)
2835
2836        with _bms.charger(16.8, 0.200):
2837            logger.write_result_to_html_report("Charging at 100 mA, 4.205+ V")
2838            self.set_exact_volts(test_cell, 4.22, 0)
2839            serial_watcher.assert_true("flags.prefault_overvoltage_charge", True, 2)
2840            serial_watcher.assert_true("flags.fault_overvoltage_charge", True, 2)
2841
2842            logger.write_result_to_html_report("Charging at 100 mA, 4.10 V")
2843            self.set_exact_volts(test_cell, 4.10, 0)
2844            serial_watcher.assert_true("flags.prefault_overvoltage_charge", False, 3)
2845            serial_watcher.assert_true("flags.fault_overvoltage_charge", False, 3)
2846
2847            logger.write_result_to_html_report("Charging at 100 mA, 4.25+ V")
2848            self.set_exact_volts(test_cell, 4.27, 0)
2849            serial_watcher.assert_true("flags.permanentdisable_overvoltage", True, 2)
2850
2851            logger.write_result_to_html_report("Charging at 100 mA, 4.10 V")
2852            self.set_exact_volts(test_cell, 4.10, 0)
2853            serial_watcher.assert_false("flags.permanentdisable_overvoltage", False, 3)
2854
2855    def test_undervoltage_faults(self, serial_watcher: SerialWatcher):
2856        """
2857        | Description          | Test under voltage faults                                             |
2858        | :------------------- | :--------------------------------------------------------------------- |
2859        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2860        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2861jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2862        | MIL-PRF Sections     | 4.6.1 (Standard Charge)                                                |
2863        | Instructions         | 1. Rest at 0 mA, 3.0 V                                            </br>\
2864                                 2. Rest at 0 mA, 2.3 V                                            </br>\
2865                                 3. Rest at 0 mA, 2.6 V                                            </br>\
2866                                 4. Charge at 500 mA, 2.3 V                                        </br>\
2867                                 5. Charge at 500 mA, 2.4 V                                        </br>\
2868                                 6. Charge at 500 mA, 2.2 V                                        </br>\
2869                                 5. Charge at 500 mA, 2.6 V                                             |
2870        | Pass / Fail Criteria | ⦁ Prefault under voltage discharge is False at 0mA, 3.0 V         </br>\
2871                                 ⦁ Fault slumber under voltage discharge is False at 0mA, 3.0 V    </br>\
2872                                 ⦁ Prefault under voltage charge is False at 0mA, 3.0 V            </br>\
2873                                 ⦁ Permanent disable under voltage is False at 0mA, 3.0 V          </br>\
2874                                 ⦁ Prefault under voltage discharge is True when resting at 0mA, 2.3 V  </br>\
2875                                 ⦁ Fault slumber under voltage is True when resting at 0mA, 2.3 V  </br>\
2876                                 ⦁ Prefault under voltage discharge is False when resting at 0mA, 2.6 V </br>\
2877                                 ⦁ Fault slumber under voltage is False when resting at 0mA, 2.6 V </br>\
2878                                 ⦁ Prefault under voltage charge is True when charging at 500mA, 2.3 V  </br>\
2879                                 ⦁ Fault slumber under voltage is True when charging at 500mA, 2.3 V    </br>\
2880                                 ⦁ Prefault under voltage discharge is False when charging at 500mA, 2.4 V  </br>\
2881                                 ⦁ Fault slumber under voltage is False when charging at 500mA, 2.4 V   </br>\
2882                                 ⦁ Permanent disable under voltage is True when charging at 500mA, 2.2 V    </br>\
2883                                 ⦁ Permanent disable under voltage is False when charging at 500mA, 2.6 V   |
2884        | Estimated Duration   | 10 seconds                                                             |
2885        | Note                 | This has also been validated in software, meaning the logic should     \
2886                                 properly handle a situation with a cell discharging too low, however   \
2887                                 this has not yet been tested in hardware with a cell sensor reading    \
2888                                 that low of voltage and triggering a fault. If we are reading less     \
2889                                 than 20mamps from the cells, we should be able to trigger an           \
2890                                 under-voltage fault. If we read less than 2.4 volts, we must           \
2891                                 trigger a fault. If this fault persists for over 1 second, we          \
2892                                 should then trigger a full fault. We will not clear this fault         \
2893                                 unless we are able to read above 2.5 volts. If we are reading over     \
2894                                 20 mamps and a cell reads less than 2.325 volts, we must trigger a     \
2895                                 cell voltage charge min prefault, if this persists for another bms     \
2896                                 software cycle we will trigger a full fault. This fault will clear     \
2897                                 when we read above this voltage. If the cell voltage ever goes under   \
2898                                 2.3 while charging, we must trigger a permanent fault.                 |
2899        """
2900        logger.write_info_to_report("Testing Undervoltage")
2901        assert _bms.load and _bms.charger  # Confirm hardware is available
2902        test_cell = _bms.cells[1]
2903        for cell in _bms.cells.values():
2904            cell.disengage_safety_protocols = True
2905            # Must be low enough to not trigger cell imbalance [abs(high - low) > 0.5V]
2906            self.set_exact_volts(cell, 2.6, 0.00)
2907
2908        logger.write_result_to_html_report("Resting at 0 mA, 3.0 V")
2909        serial_watcher.assert_true("flags.prefault_undervoltage_discharge", False, 1)
2910        serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False, 1)
2911        serial_watcher.assert_true("flags.prefault_undervoltage_charge", False, 1)
2912        serial_watcher.assert_true("flags.fault_undervoltage_charge", False, 1)
2913        serial_watcher.assert_true("flags.permanentdisable_undervoltage", False, 1)
2914
2915        with _bms.load(0.500):
2916            logger.write_result_to_html_report("Discharging at -500 mA, 2.325 V")
2917            self.set_exact_volts(test_cell, 2.320, 0.00)
2918            serial_watcher.assert_true("flags.prefault_undervoltage_discharge", True, 2, wait_time=600)
2919            serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", True, 2, wait_time=600)
2920
2921            logger.write_result_to_html_report("Discharging at -500 mA, 2.6 V")
2922            self.set_exact_volts(test_cell, 2.6, 0.00)
2923            serial_watcher.assert_true("flags.prefault_undervoltage_discharge", False, 3, wait_time=600)
2924            serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False, 3, wait_time=600)
2925
2926        with _bms.charger(16.8, 0.500):
2927            logger.write_result_to_html_report("Charging at 500 mA, 2.325 V")
2928            self.set_exact_volts(test_cell, 2.320, 0.00)
2929            serial_watcher.assert_true("flags.prefault_undervoltage_charge", True, 2, wait_time=600)
2930            serial_watcher.assert_true("flags.fault_undervoltage_charge", True, 2, wait_time=600)
2931
2932            logger.write_result_to_html_report("Charging at 500 mA, 2.4 V")
2933            self.set_exact_volts(test_cell, 2.6, 0.00)
2934            serial_watcher.assert_true("flags.prefault_undervoltage_charge", False, 3)
2935            serial_watcher.assert_true("flags.fault_undervoltage_charge", False, 3)
2936
2937            logger.write_result_to_html_report("Charging at 500 mA, 2.2 V")
2938            self.set_exact_volts(test_cell, 2.2, 0.00)
2939            serial_watcher.assert_true("flags.permanentdisable_undervoltage", True, 2)
2940
2941            logger.write_result_to_html_report("Charging at 500 mA, 2.6 V")
2942            self.set_exact_volts(test_cell, 2.6, 0.00)
2943            serial_watcher.assert_false("flags.permanentdisable_undervoltage", False, 3)
2944
2945    def test_cell_imbalance(self, serial_watcher: SerialWatcher):
2946        """
2947        | Description          | Test Cell Imbalance                                                    |
2948        | :------------------- | :--------------------------------------------------------------------- |
2949        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2950        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2951jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2952        | MIL-PRF Sections     | 4.7.2.1 (Cell balance)                                                 |
2953        | Instructions         | 1. Rest at 0 mA, 3.8 V                                            </br>\
2954                                 2. Rest at 0 mA, 2.0 V                                            </br>\
2955                                 3. Rest at 0 mA, 3.8 V                                                 |
2956        | Pass / Fail Criteria | ⦁ Prefault cell imbalance is False after resting at 0mA, 3.8 V    </br>\
2957                                 ⦁ Permanent disable cell imbalance is False after resting at 0mA, 3.8 V    </br>\
2958                                 ⦁ Prefault cell imbalance is True after resting at 0mA, 2.0 V     </br>\
2959                                 ⦁ Permanent disable cell imbalance is True after resting at 0mA, 2.0 V    </br>\
2960                                 ⦁ Permanent disable cell imbalance is False after resting at 0mA, 3.8 V |
2961        | Estimated Duration   | 10 seconds                                                             |
2962        | Note                 | Occurs when the difference between the highest and lowest cell is 0.5V.|
2963        """
2964        logger.write_info_to_report("Testing Cell Imbalance")
2965        assert _bms.load and _bms.charger  # Confirm hardware is available
2966        test_cell = _bms.cells[1]
2967        test_cell.disengage_safety_protocols = True
2968
2969        # Set all cell voltages
2970        for cell in _bms.cells.values():
2971            cell.volts = CELL_VOLTAGE
2972
2973        voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
2974        logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
2975        serial_watcher.assert_true("flags.permanentdisable_cellimbalance", False, 1)
2976
2977        self.set_exact_volts(test_cell, 2.0)
2978        voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
2979        logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
2980        serial_watcher.assert_true("flags.permanentdisable_cellimbalance", True, 2)
2981
2982        self.set_exact_volts(test_cell, CELL_VOLTAGE)
2983        voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
2984        logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
2985        serial_watcher.assert_false("flags.permanentdisable_cellimbalance", False, 3)
2986
2987    def test_overvoltage_overtemp_faults(self, serial_watcher: SerialWatcher):
2988        """
2989        | Description          | Test combined high temperature & high voltage                          |
2990        | :------------------- | :--------------------------------------------------------------------- |
2991        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2992        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2993jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2994        | MIL-PRF Sections     | 4.7.3.3 (Extreme high temperature discharge)                           |
2995        | Instructions         | 1. Rest at 0 mA, 3.8002 V, 15°C                                   </br>\
2996                                 2. Charge at 100 mA, 4.21 V, 54°C                                 </br>\
2997                                 3. Charge at 100 mA, 4.10 V, 15°C                                 </br>\
2998                                 4. Charge at 100 mA, 4.26 V, 94°C                                 </br>\
2999                                 5. Charge at 100 mA, 4.10 V, 15°C                                      |
3000        | Pass / Fail Criteria | ⦁ Prefault over voltage charge is False at 0 mA, 3.8002 V, 15°C   </br>\
3001                                 ⦁ Fault over voltage charge is False at 0 mA, 3.8002 V, 15°C      </br>\
3002                                 ⦁ Permanent disable over votlage charge is False at 0 mA, 3.8002 V, 15°C </br>\
3003                                 ⦁ Prefault over voltage charge is True at 4.21 V, 54°C            </br>\
3004                                 ⦁ Fault over voltage charge is True at 4.21 V, 54°C               </br>\
3005                                 ⦁ Prefault over temperature charge is True at 4.21 V, 54°C        </br>\
3006                                 ⦁ Fault over temperature charge is True at 4.21 V, 54°C           </br>\
3007                                 ⦁ Prefault over voltage charge is False at 4.10 V, 15°C           </br>\
3008                                 ⦁ Prefault over temperature charge is False at 4.10 V, 15°C       </br>\
3009                                 ⦁ Fault over voltage charge is False at 4.10 V, 15°C              </br>\
3010                                 ⦁ Fault over temperature charge is False at 4.10 V                </br>\
3011                                 ⦁ Permanent disable over voltage is True at 4.26 V, 94°C          </br>\
3012                                 ⦁ Permanent disable over temperature is True at 4.26 V, 94°C      </br>\
3013                                 ⦁ Permanent disable over voltage is False at  4.10 V, 15°C        </br>\
3014                                 ⦁ Permanent disable over temperature is False at  4.10 V, 15°C         |
3015        | Estimated Duration   | 10 seconds                                                             |
3016        | Note                 | Combine over voltage and over temperature faults                       |
3017        """
3018        logger.write_info_to_report("Testing Overvoltage with Overtemp")
3019        assert _bms.load and _bms.charger  # Confirm hardware is available
3020        test_cell = _bms.cells[1]
3021        for cell in _bms.cells.values():
3022            cell.disengage_safety_protocols = True
3023            # Must be high enough to not trigger cell imbalance [abs(high - low) > 0.5V]
3024            self.set_exact_volts(cell, 4.0, 0.0)
3025
3026        logger.write_result_to_html_report(f"Resting at 0 mA, {CELL_VOLTAGE} V, 15°C")
3027        serial_watcher.assert_true("flags.prefault_overvoltage_charge", False, 1)
3028        serial_watcher.assert_true("flags.fault_overvoltage_charge", False, 1)
3029        serial_watcher.assert_true("flags.permanentdisable_overvoltage", False, 1)
3030
3031        serial_watcher.assert_true("flags.prefault_overtemp_charge", False, 1)
3032        serial_watcher.assert_true("flags.fault_overtemp_charge", False, 1)
3033        serial_watcher.assert_true("flags.permanentdisable_overtemp", False, 1)
3034
3035        with _bms.charger(16.8, 0.200):
3036            logger.write_result_to_html_report("Charging at 100 mA, 4.21 V, 54°C")
3037            self.set_exact_volts(test_cell, 4.22, 0.0)
3038            _plateset.thermistor1 = 54
3039            serial_watcher.assert_true("flags.prefault_overvoltage_charge", True, 2)
3040            serial_watcher.assert_true("flags.fault_overvoltage_charge", True, 2)
3041            serial_watcher.assert_true("flags.prefault_overtemp_charge", True, 2)
3042            serial_watcher.assert_true("flags.fault_overtemp_charge", True, 2)
3043
3044            logger.write_result_to_html_report("Charging at 100 mA, 4.10 V, 15°C")
3045            self.set_exact_volts(test_cell, 4.10, 0.0)
3046            _plateset.thermistor1 = 15
3047            serial_watcher.assert_true("flags.prefault_overvoltage_charge", False, 3)
3048            serial_watcher.assert_true("flags.fault_overvoltage_charge", False, 3)
3049            serial_watcher.assert_true("flags.prefault_overtemp_charge", False, 3)
3050            serial_watcher.assert_true("flags.fault_overtemp_charge", False, 3)
3051
3052    def test_undervoltage_undertemp_faults(self, serial_watcher: SerialWatcher):
3053        """
3054        | Description          | Test combined low temperature & low voltage                            |
3055        | :------------------- | :--------------------------------------------------------------------- |
3056        | GitHub Issue         | turnaroundfactor/HITL#476                                              |
3057        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3058jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
3059        | MIL-PRF Sections     | 4.7.3.2 (Extreme low temperature Discharge)                            |
3060        | Instructions         | 1. Rest at 0 mA, 3.0 V, 15°C                                      </br>\
3061                                 2. Rest at 0 mA, 2.3 V, -21°C                                     </br>\
3062                                 3. Rest at 0 mA, 2.6 V, -17°C                                     </br>\
3063                                 4. Charge at 500 mA, 2.3 V, -25°C                                  </br>\
3064                                 5. Charge at 500 mA, 2.4 V, -20°C                                  </br>\
3065                                 6. Charge at 500 mA, 2.2 V,  15°C                                 </br>\
3066                                 7. Charge at 500 mA, 2.6 V,  15°C                                      |
3067        | Pass / Fail Criteria | ⦁ Prefault under voltage discharge is False at 0 mA, 3.0 V, 15°C  </br>\
3068                                 ⦁ Fault slumber under voltage charge is False at 0 mA, 3.0 V, 15°C</br>\
3069                                 ⦁ Prefault under voltage charge is False at 0 mA, 3.0 V, 15°C     </br>\
3070                                 ⦁ Fault under voltage charge is False at 0 mA, 3.0 V, 15°C        </br>\
3071                                 ⦁ Permanent disable under voltage is False at 0 mA, 3.0 V, 15°C   </br>\
3072                                 ⦁ Prefault under temperature discharge is False at 0 mA, 3.0 V, 15°C </br>\
3073                                 ⦁ Fault under temperature discharge is False at 0 mA, 3.0 V, 15°C </br>\
3074                                 ⦁ Prefault under temperature charge is False at 0 mA, 3.0 V, 15°C </br>\
3075                                 ⦁ Fault under temperature charge is False at 0 mA, 3.0 V, 15°C    </br>\
3076                                 ⦁ Fault under voltage charge is False at 0 mA, 3.0 V, 15°C        </br>\
3077                                 ⦁ Fault under voltage charge is False at 0 mA, 3.0 V, 15°C        </br>\
3078                                 ⦁ Prefault under voltage discharge is True at 0 mA, 2.3 V, -21°C  </br>\
3079                                 ⦁ Fault slumber under voltage discharge is True at 0 mA, 2.3 V, -21°C </br>\
3080                                 ⦁ Prefault under temperature discharge is True at 0 mA, 2.3 V, -21°C  </br>\
3081                                 ⦁ Fault under temperature discharge is True at 0 mA, 2.3 V, -21°C </br>\
3082                                 ⦁ Prefault under voltage discharge is False at 0 mA, 2.6 V, -17°C </br>\
3083                                 ⦁ Fault slumber under voltage discharge is False at 0 mA, 2.6 V, -17°C </br>\
3084                                 ⦁ Prefault under temperature discharge is False at 0 mA, 2.6 V, -17°C  </br>\
3085                                 ⦁ Fault under temperature discharge is False at 0 mA, 2.6 V, -17°C</br>\
3086                                 ⦁ Prefault under voltage charge is True at 500 mA, 2.3 V, -25°C    </br>\
3087                                 ⦁ Fault slumber under voltage charge is True at 500 mA, 2.3 V, -25°C</br>\
3088                                 ⦁ Prefault under temperature charge is True at 500 mA, 2.3 V, -25°C</br>\
3089                                 ⦁ Fault under temperature charge is True at 500 mA, 2.3 V, -25°C   </br>\
3090                                 ⦁ Prefault under voltage charge is False at 500 mA, 2.4 V, -20°C    </br>\
3091                                 ⦁ Fault slumber under voltage charge is False at 500 mA, 2.4 V, -20°C </br>\
3092                                 ⦁ Prefault under temperature charge is False at 500 mA, 2.4 V, -20°C</br>\
3093                                 ⦁ Fault under temperature charge is False at 500 mA, 2.4 V, -20°C   </br>\
3094                                 ⦁ Permanent disable under voltage is True at 500 mA, 2.2 V, 15°C  </br>\
3095                                 ⦁ Permanent disable under voltage is False at 500 mA, 2.6 V, 15°C      |
3096        | Estimated Duration   | 10 seconds                                                             |
3097        | Note                 | Combine under voltage and under temperature faults                     |
3098        """
3099
3100        logger.write_info_to_report("Testing Undervoltage with Undertemp")
3101        assert _bms.load and _bms.charger  # Confirm hardware is available
3102        test_cell = _bms.cells[1]
3103        for cell in _bms.cells.values():
3104            cell.disengage_safety_protocols = True
3105            self.set_exact_volts(
3106                cell, 2.8, 0
3107            )  # Must be low enough to not trigger cell imbalance [abs(high - low) > 0.5V]
3108
3109        logger.write_result_to_html_report("Resting at 0 mA, 3.0 V, 15°C")
3110        _plateset.thermistor1 = 15
3111        serial_watcher.assert_true("flags.prefault_undervoltage_discharge", False, 1)
3112        serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False, 1)
3113        serial_watcher.assert_true("flags.prefault_undervoltage_charge", False, 1)
3114        serial_watcher.assert_true("flags.fault_undervoltage_charge", False, 1)
3115        serial_watcher.assert_true("flags.permanentdisable_undervoltage", False, 1)
3116
3117        serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 1)
3118        serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 1)
3119        serial_watcher.assert_true("flags.prefault_undertemp_charge", False, 1)
3120        serial_watcher.assert_true("flags.fault_undertemp_charge", False, 1)
3121
3122        with _bms.load(0.500):
3123            logger.write_result_to_html_report("Discharging at -500 mA, 2.325 V, -35°C")
3124            self.set_exact_volts(test_cell, 2.320, 0)
3125            _plateset.thermistor1 = -35
3126            serial_watcher.assert_true("flags.prefault_undervoltage_discharge", True, 2)
3127            serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", True, 2)
3128            serial_watcher.assert_true("flags.prefault_undertemp_discharge", True, 2)
3129            serial_watcher.assert_true("flags.fault_undertemp_discharge", True, 2)
3130
3131            logger.write_result_to_html_report("Discharging at -500 mA, 2.6 V, -30°C")
3132            self.set_exact_volts(test_cell, 2.8, 0)
3133            _plateset.thermistor1 = -30
3134            serial_watcher.assert_true("flags.prefault_undervoltage_discharge", False, 3)
3135            serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False, 3)
3136            serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 3)
3137            serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 3)
3138
3139        with _bms.charger(16.8, 0.500):
3140            logger.write_result_to_html_report("Charging at 500 mA, 2.325 V, -25°C")
3141            self.set_exact_volts(test_cell, 2.320, 0.00)
3142            _plateset.thermistor1 = -25
3143            serial_watcher.assert_true("flags.prefault_undervoltage_charge", True, 2)
3144            serial_watcher.assert_true("flags.fault_undervoltage_charge", True, 2)
3145            serial_watcher.assert_true("flags.prefault_undertemp_charge", True, 2)
3146            serial_watcher.assert_true("flags.fault_undertemp_charge", True, 2)
3147
3148            logger.write_result_to_html_report("Charging at 500 mA, 2.6 V, -20°C")
3149            self.set_exact_volts(test_cell, 2.8, 0)
3150            _plateset.thermistor1 = -20
3151            serial_watcher.assert_true("flags.prefault_undervoltage_charge", False, 3)
3152            serial_watcher.assert_true("flags.fault_undervoltage_charge", False, 3)
3153            serial_watcher.assert_true("flags.prefault_undertemp_charge", False, 3)
3154            serial_watcher.assert_true("flags.fault_undertemp_charge", False, 3)
3155
3156    def test_cell_imbalance_charge(self, serial_watcher: SerialWatcher):
3157        """
3158        | Description          | Test Cell Imbalance                                                    |
3159        | :------------------- | :--------------------------------------------------------------------- |
3160        | GitHub Issue         | turnaroundfactor/HITL#476                                              |
3161        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3162jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
3163        | MIL-PRF Sections     | 4.7.2.1 (Cell balance)                                                 |
3164        | Instructions         | 1. Rest at 0 mA                                                   </br>\
3165                                 2. Charge at 1 mA                                                 </br>\
3166                                 3. Rest at 0 mA                                                        |
3167        | Pass / Fail Criteria | ⦁ Prefault cell imbalance is False after resting at 0mA           </br>\
3168                                 ⦁ Permanent disable cell imbalance is False after resting at 0mA  </br>\
3169                                 ⦁ Prefault cell imbalance is True after resting at 1 A            </br>\
3170                                 ⦁ Permanent disable cell imbalance is True after resting 1 A      </br>\
3171                                 ⦁ Prefault cell imbalance is True after resting at 0 A            </br>\
3172                                 ⦁ Permanent disable cell imbalance is False after resting at 0 A       |
3173        | Estimated Duration   | 10 seconds                                                             |
3174        | Note                 | Occurs when the difference between the highest and lowest cell is 0.5V.|
3175        """
3176        logger.write_info_to_report("Testing Cell Imbalance Charge")
3177        assert _bms.load and _bms.charger  # Confirm hardware is available
3178
3179        test_cell = _bms.cells[1]
3180        test_cell.disengage_safety_protocols = True
3181        for cell in _bms.cells.values():
3182            cell.exact_volts = 4.2
3183
3184        voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
3185        logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
3186        serial_watcher.assert_true("flags.permanentdisable_cellimbalance", False, 1)
3187
3188        with _bms.charger(16.8, 1):
3189            self.set_exact_volts(test_cell, 2.5)
3190            voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
3191            logger.write_result_to_html_report(f"Charging at 1 A, {', '.join(voltages)}")
3192            serial_watcher.assert_true("flags.permanentdisable_cellimbalance", True)
3193
3194            test_cell.exact_volts = 4.2
3195            voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
3196            logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
3197            serial_watcher.assert_false("flags.permanentdisable_cellimbalance", True)
3198
3199    def test_current_limit(self, serial_watcher: SerialWatcher):
3200        """
3201        | Description          | Confirm A fault is raised after 3.5A                                                 |
3202        | :------------------- | :----------------------------------------------------------------------------------- |
3203        | GitHub Issue         | turnaroundfactor/HITL#476                                                            |
3204        | Instructions         | 1. Rest for 30 second                                                           </br>\
3205                                 2. Charge at 3.75 amps (the max limit will be 3.5 amps)                         </br>\
3206                                 3. Verify a prefault and fault are raised.                                      </br>\
3207                                 4. Rest until faults clear                                                      </br>\
3208                                 5. Charge at 3.75 amps                                                          </br>\
3209                                 6. Verify a prefault occurred but not a fault                                        |
3210        | Pass / Fail Criteria | Pass if faults are raised                                                            |
3211        | Estimated Duration   | 2 minutes                                                                            |
3212        | Notes                | We use 3.5A as a limit due to hardware limitations                                   |
3213        """
3214
3215        logger.write_info_to_report("Testing Current Limit")
3216        assert _bms.load and _bms.charger  # Confirm hardware is available
3217
3218        # Rest and make sure no faults are active
3219        logger.write_result_to_html_report("Confirm no faults are active")
3220        serial_watcher.assert_true("flags.prefault_overcurrent_charge", False, 1)
3221        serial_watcher.assert_true("flags.fault_overcurrent_charge", False, 1)
3222
3223        # Charge at 3.25 amps and verify faults are raised
3224        logger.write_result_to_html_report("Charging 3.75 A")
3225        with _bms.charger(16.8, 3.75):
3226            serial_watcher.assert_true("flags.prefault_overcurrent_charge", True, 2, wait_time=60)
3227            serial_watcher.assert_true("flags.fault_overcurrent_charge", True, 2, wait_time=60)
3228
3229        # Rest until faults clear
3230        logger.write_result_to_html_report("Resting")
3231        serial_watcher.assert_true("flags.prefault_overcurrent_charge", False, 3)
3232        serial_watcher.assert_true("flags.fault_overcurrent_charge", False, 3)
3233        assert (
3234            serial_watcher.events["flags.fault_overcurrent_charge"][2].bms_time
3235            - serial_watcher.events["flags.fault_overcurrent_charge"][1].bms_time
3236        ).total_seconds() >= 60
3237
3238    def test_undertemp_charge_rate(self, serial_watcher: SerialWatcher):
3239        """
3240        | Description          | Confirm a fault is raised at or above 3.1A when below 5°C                            |
3241        | :------------------- | :----------------------------------------------------------------------------------- |
3242        | GitHub Issue         | turnaroundfactor/HITL#611                                                            |
3243        | Instructions         | 1. Rest for 30 second                                                           </br>\
3244                                 2. Charge at 3.3 amps below 5°C                                                 </br>\
3245                                 3. Verify a prefault (before 1 second) and fault (after 1 second) are raised.   </br>\
3246                                 4. Charge above 7°C until faults clear                                               |
3247        | Pass / Fail Criteria | Pass if faults are raised                                                            |
3248        | Estimated Duration   | 2 minutes                                                                            |
3249        """
3250
3251        logger.write_info_to_report("Testing Undertemp charge rate")
3252        assert _bms.load and _bms.charger  # Confirm hardware is available
3253
3254        # Rest and make sure no faults are active
3255        logger.write_result_to_html_report("Confirm no faults are active")
3256        serial_watcher.assert_true("flags.prefault_undertemp_charge_rate", False, 1)
3257        serial_watcher.assert_true("flags.fault_undertemp_charge_rate", False, 1)
3258
3259        # Discharge at 3.1+ amps and verify faults are raised
3260        logger.write_result_to_html_report("Discharging at 3.1A+")
3261        with _bms.charger(16.8, 3.3):
3262            _plateset.thermistor1 = 3
3263            serial_watcher.assert_true("flags.prefault_undertemp_charge_rate", True, 2, wait_time=60)
3264            serial_watcher.assert_true("flags.fault_undertemp_charge_rate", True, 2, wait_time=60)
3265            _plateset.thermistor1 = 8
3266            serial_watcher.assert_true("flags.prefault_undertemp_charge_rate", False, 3, wait_time=90)
3267            serial_watcher.assert_true("flags.fault_undertemp_charge_rate", False, 3, wait_time=90)
3268
3269    def test_overcurrent_discharge_sustained(self, serial_watcher: SerialWatcher):
3270        """
3271        | Description          | Test Over Current Discharge Sustained Fault                            |
3272        | :------------------- | :--------------------------------------------------------------------- |
3273        | GitHub Issue         | turnaroundfactor/HITL#762                                              |
3274        | Instructions         | 1. Check prefault_sw_overcurrent_discharge and fault_sw_overcurrent_discharge </br>\
3275                                 2. Set current to less than -2.5 amps                             </br>\
3276                                 3. Confirm faults listed in #1 are true                           </br>\
3277                                 4. Set current to 0 amps (rest)                                   </br>\
3278                                 5. Confirm faults listed in #1 are false                               |
3279        | Pass / Fail Criteria | ⦁ Prefault overCurrent Discharge Sustained is False at start      </br>\
3280                                 ⦁ Fault overCurrent Discharge Sustained is False at start         </br>\
3281                                 ⦁ Prefault overCurrent Discharge Sustained is True when current is -2.5 A </br>\
3282                                 ⦁ Fault overCurrent Discharge Sustained is True when current is -2.5 A  </br>\
3283                                 ⦁ Prefault overCurrent Discharge Sustained is False when current is 0 A </br>\
3284                                 ⦁ Fault overCurrent Discharge Sustained is False when current is 0 A   |
3285        | Estimated Duration   | 10 seconds                                                             |
3286        """
3287
3288        logger.write_info_to_report("Testing Overcurrent Discharge Sustained Faults")
3289        assert _bms.load and _bms.charger  # Confirm hardware is available
3290
3291        logger.write_info_to_report("Sw overcurrent protection high current short time")
3292        serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_2", False, 1)
3293        serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_2", False, 1)
3294
3295        logger.write_result_to_html_report("Setting current to less than -2.6 amps")
3296        with _bms.load(2.6):
3297            serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_2", True, 2)
3298            serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_2", True, 2)
3299
3300        logger.write_result_to_html_report("Resting")
3301        serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_2", False, 3, wait_time=60 * 25)
3302        serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_2", False, 3, wait_time=60 * 25)
3303
3304        logger.write_info_to_report("Sw overcurrent protection low current longer time")
3305        serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_1", False, 1)
3306        serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_1", False, 1)
3307
3308        logger.write_result_to_html_report("Setting current to less than -2.4 amps")
3309        with _bms.load(2.4):
3310            serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_1", True, 2)
3311            serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_1", True, 2)
3312
3313        logger.write_result_to_html_report("Resting")
3314        serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_1", False, 3, wait_time=60 * 10)
3315        serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_1", False, 3, wait_time=60 * 10)
3316
3317
3318@pytest.mark.parametrize("reset_test_environment", [{"volts": 2.5}], indirect=True)
3319class TestStateOfCharge:
3320    """Confirm cell sim SOC and BMS SOC are within 5%"""
3321
3322    def test_state_of_charge(self):
3323        """
3324        | Description          | Confirm cell sim state of charge and BMS state of charge are within 5% |
3325        | :------------------- | :--------------------------------------------------------------------- |
3326        | GitHub Issue         | turnaroundfactor/HITL#474                                              |
3327        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3328jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D19) |
3329        | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
3330        | Instructions         | 1. Set cell sims SOC to 5%                                        </br>\
3331                                 2. Confirm serial SOC is within 5%                                </br>\
3332                                 3. Increment the cell sim SOC by 1% and repeat steps 1 & 2.            |
3333        | Pass / Fail Criteria | ⦁ Serial state of charge is within 5%                                  |
3334        | Estimated Duration   | 20 seconds                                                             |
3335        | Note                 | When tested as specified in MIL-PERF section 4.7.2.15.1, SMBus data    \
3336                                 output shall be accurate within +0/-5% of the actual state of charge   \
3337                                 for the battery under test throughout the discharge. Manufacturer and  \
3338                                 battery data shall be correctly programmed (see 4.7.2.15.1).           |
3339        """
3340
3341        percent_failed = []
3342        allowed_error = 0.05
3343        max_error = 0.0
3344        failure_rate = []
3345
3346        for percent in range(5, 101):
3347            logger.write_info_to_report(f"Setting cell sims SOC to {percent}%")
3348
3349            for cell in _bms.cells.values():
3350                cell.state_of_charge = percent / 100
3351
3352            time.sleep(2)
3353            serial_monitor.read()  # Clear the latest serial buffer
3354
3355            serial_data = serial_monitor.read()
3356            percent_charged = serial_data["percent_charged"]
3357
3358            max_error = max(max_error, abs(percent_charged - percent))
3359            if not (percent - 5) <= percent_charged <= (percent + 5):
3360                failure_rate.append(1)
3361                logger.write_warning_to_report(
3362                    f"State of Charge is not within {allowed_error:.0%} of {percent}, received {percent_charged}%"
3363                )
3364                percent_failed.append(str(percent))
3365            else:
3366                failure_rate.append(0)
3367                logger.write_info_to_report(
3368                    f"State of Charge is within {allowed_error:.0%} of {percent}% after changing cell sims SOC"
3369                )
3370
3371        if len(percent_failed) > 0:
3372            message = (
3373                f"SOC Error: {max_error / 100:.1%}{allowed_error:.0%} "
3374                f"[{sum(failure_rate) / len(failure_rate):.1%} failed]"
3375            )
3376            logger.write_result_to_html_report(message)
3377            pytest.fail(message)
3378        else:
3379            logger.write_result_to_html_report(f"SOC Error: {max_error:.1%}{allowed_error:.0%}")
3380
3381
3382@pytest.mark.parametrize("reset_test_environment", [{"volts": 3.7, "temperature": 23}], indirect=True)
3383class TestTemperatureAccuracy:
3384    """Compare BMS and HITL temps."""
3385
3386    class TemperatureDiscrepancyTherm1(CSVRecordEvent):
3387        """@private Compare HITL temperature to reported temperature."""
3388
3389        allowable_error = 5.0
3390        max = SimpleNamespace(hitl_c=0, bms_c=0, error=0.0)
3391
3392        @classmethod
3393        def failed(cls) -> bool:
3394            """Check if test parameters were exceeded."""
3395            return bool(cls.max.error > cls.allowable_error)
3396
3397        @classmethod
3398        def verify(cls, _row, serial_data, _cell_data):
3399            """Temperature within range"""
3400            row_data = SimpleNamespace(hitl_c=_plateset.thermistor1, bms_c=serial_data["dk_temp"] / 10 - 273)
3401            row_data.error = abs(row_data.bms_c - row_data.hitl_c)
3402            cls.max = max(cls.max, row_data, key=lambda data: data.error)
3403
3404        @classmethod
3405        def result(cls):
3406            """Detailed test result information."""
3407            return (
3408                f"Thermistor 1 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error, '°C', '.2f')}"
3409                f"(HITL: {cls.max.hitl_c:.2f} °C, BMS: {cls.max.bms_c:.2f} °C)"
3410            )
3411
3412    class TemperatureDiscrepancyTherm2(CSVRecordEvent):
3413        """@private Compare HITL temperature to reported temperature."""
3414
3415        allowable_error = 5.0
3416        max = SimpleNamespace(hitl_c=0, bms_c=0, error=0.0)
3417
3418        @classmethod
3419        def failed(cls) -> bool:
3420            """Check if test parameters were exceeded."""
3421            return bool(cls.max.error > cls.allowable_error)
3422
3423        @classmethod
3424        def verify(cls, _row, serial_data, _cell_data):
3425            """Temperature within range"""
3426            row_data = SimpleNamespace(hitl_c=_plateset.thermistor2, bms_c=serial_data["dk_temp1"] / 10 - 273)
3427            row_data.error = abs(row_data.bms_c - row_data.hitl_c)
3428            cls.max = max(cls.max, row_data, key=lambda data: data.error)
3429
3430        @classmethod
3431        def result(cls):
3432            """Detailed test result information."""
3433            return (
3434                f"Thermistor 2 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error, '°C', '.2f')}"
3435                f"(HITL: {cls.max.hitl_c:.2f} °C, BMS: {cls.max.bms_c:.2f} °C)"
3436            )
3437
3438    def test_temperature_accuracy(self):
3439        """
3440         | Description          | TAF: Ensure that temperature measurements are accurate                       |
3441         | :------------------- | :--------------------------------------------------------------------------- |
3442         | GitHub Issue         | turnaroundfactor/HITL#401                                                    |
3443         | MIL-PRF Section      | 3.5.8.3 (Accuracy)                                                      </br>\
3444                                  4.7.2.14.3 (Accuracy During Discharge)                                       |
3445         | Instructions         | 1. Set THERM1 and THERM2 to -40C                                        </br>\
3446                                  2. Set cell voltages to 3.7V per cell                                   </br>\
3447                                  3. Increment THERM1 and THERM2 in 5C increments up to and including 60C </br>\
3448                                  4.  Record the following data at each current increment                 </br>\
3449                                      HITL: THERM1 Measurement, THERM2 Measurement                        </br>\
3450                                      SERIAL: THERM1, THERM2"                                                  |
3451         | Pass / Fail Criteria | HITL and Serial are within 5%                                                |
3452         | Estimated Duration   | 1 minute                                                                     |
3453         """
3454        _bms.timer.reset()  # Keep track of runtime
3455        _plateset.disengage_safety_protocols = True
3456
3457        for target_c in range(-40, 65, 5):
3458            _plateset.thermistor1 = _plateset.thermistor2 = target_c
3459            time.sleep(1)
3460            logger.write_info_to_report(
3461                f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, "
3462                f"Temp1: {_plateset.thermistor1}, Temp2: {_plateset.thermistor2}"
3463            )
3464            _bms.csv.cycle.record(_bms.timer.elapsed_time)
3465            time.sleep(0.5)
3466
3467        # Check results
3468        if CSVRecordEvent.failed():
3469            pytest.fail(CSVRecordEvent.result())
3470
3471
3472@pytest.mark.parametrize("reset_test_environment", [{"volts": 3.7, "temperature": 23}], indirect=True)
3473class TestCurrentAccuracy:
3474    """Run a test for current accuracy"""
3475
3476    class CurrentCellAccuracy(CSVRecordEvent):
3477        """@private Compare terminal current to reported current."""
3478
3479        allowable_error = 0.01
3480        max = SimpleNamespace(hitl_a=0, bms_a=0, error=0.0)
3481
3482        @classmethod
3483        def failed(cls) -> bool:
3484            """Check if test parameters were exceeded."""
3485            return bool(cls.max.error > cls.allowable_error)
3486
3487        @classmethod
3488        def verify(cls, row, serial_data, _cell_data):
3489            """Current within range"""
3490            if not _plateset.load_switch:
3491                row_data = SimpleNamespace(hitl_a=row["HITL Current (A)"], bms_a=serial_data["mamps"] / 1000)
3492            else:
3493                row_data = SimpleNamespace(hitl_a=-row["HITL Current (A)"], bms_a=serial_data["mamps"] / 1000)
3494            row_data.error = abs((row_data.bms_a - row_data.hitl_a) / row_data.hitl_a)
3495            if abs(row_data.hitl_a) > 0.100:  # Ignore currents within 100mA to -100mA
3496                cls.max = max(cls.max, row_data, key=lambda data: data.error)
3497
3498        @classmethod
3499        def result(cls):
3500            """Detailed test result information."""
3501            return (
3502                f"Current error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)} "
3503                f"(HITL: {cls.max.hitl_a * 1000:.3f} mA, BMS: {cls.max.bms_a * 1000:.3f} mA)"
3504            )
3505
3506    def test_current_accuracy(self):
3507        """
3508        | Description          | Test the cell current accuracy                                         |
3509        | :------------------- | :--------------------------------------------------------------------- |
3510        | GitHub Issue         | turnaroundfactor/HITL#400                                              |
3511        | MIL-PRF Sections     | 3.5.8.3 (Accuracy)                                                </br>\
3512                                 4.7.2.14.3 (Accuracy During Discharge)                                 |
3513        | Instructions         | 1. Set thermistors to 23C                                              |
3514                                 2. Set cell voltages to 3.7V per cell                                  |
3515                                 3. Increment the charing current from 100mA to 3A in 50mA increments   |
3516                                 4. Increment the discharging current from 100mA to 3A in 50 mA         |
3517                                        increments                                                      |
3518                                 5. Record the following data at each current increment                 |
3519                                     HITL: Current (A)                                                  |
3520                                     SERIAL: Current (A)                                                |
3521        | Pass / Fail Criteria | Pass IF:                                                               |
3522                                    SERIAL Current measurements agree with the HITL Terminal Current    |
3523                                    measurements to within 1% for abs(Terminal Current >= 100mA)        |
3524                                    - Result highest current (mA) discrepancy                           |
3525        | Estimated Duration   | ??                                                                     |
3526        | Note                 | ??                                                                     |
3527        """
3528        _bms.timer.reset()  # Keep track of runtime
3529
3530        with _bms.charger(16.8, 0.1):
3531            for target_ma in range(100, 2050, 50):
3532                _bms.charger.amps = target_ma / 1000
3533                time.sleep(1)
3534                logger.write_info_to_report(
3535                    f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Current: {_bms.charger.amps:.3f}"
3536                )
3537                time.sleep(1)
3538                _bms.csv.cycle.record(_bms.timer.elapsed_time, _bms.charger.amps)
3539                time.sleep(1)
3540
3541        with _bms.load(0.1):
3542            for target_ma in range(100, 2050, 50):
3543                time.sleep(1)
3544                _bms.load.amps = target_ma / 1000
3545                time.sleep(1)
3546                logger.write_info_to_report(
3547                    f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Current: {_bms.load.amps:.3f}"
3548                )
3549                time.sleep(1)
3550                _bms.csv.cycle.record(_bms.timer.elapsed_time, _bms.load.amps)
3551
3552        if CSVRecordEvent.failed():
3553            pytest.fail(CSVRecordEvent.result())
3554
3555
3556def battery0_voltage_check(serial_data: dict[str, int | bool | str]):
3557    """Checks the SERIAL Battery 0 Voltage"""
3558    expected_charge = 14.8
3559    voltage_low_range = expected_charge - 0.100
3560    voltage_high_range = expected_charge + 2
3561    voltage_value = f"{expected_charge}V  -100mV/+2V"
3562
3563    serial_voltage = float(serial_data["mvolt_battery"]) / 1000
3564
3565    if voltage_low_range <= serial_voltage <= voltage_high_range:
3566        logger.write_result_to_html_report(
3567            f"Battery 0 Voltage is {serial_voltage}V at the start of this test, "
3568            f"which is within within range of: {voltage_value}"
3569        )
3570
3571    else:
3572        logger.write_failure_to_html_report(
3573            f"Battery 0 Voltage is {serial_voltage}V at the start of this test, "
3574            f"which is not within within range of: {voltage_value}"
3575        )
3576        pytest.fail()
3577
3578
3579@pytest.mark.parametrize("reset_test_environment", [{"volts": 3.7, "temperature": 23}], indirect=True)
3580class TestCeaseCharging(CSVRecordEvent):
3581    """Run a test to cease charging after"""
3582
3583    def test_cease_charging(self):
3584        """
3585        | Description          | Cease charging if charger keeps trying too long                        |
3586        | :------------------- | :--------------------------------------------------------------------- |
3587        | GitHub Issue         | turnaroundfactor/HITL#745                                       |
3588        #TODO: Update MIL-PRF sections
3589        | MIL-PRF Sections     | 3.5.8.3 (Accuracy)                                                </br>\
3590                                 4.7.2.14.3 (Accuracy During Discharge)                                 |
3591        | Instructions         | 1. Set thermistors to 23C                                         </br>\
3592                                 2. Put cells in a rested state at 3.7V per cell                   </br>\
3593                                 3. Charge at 40 mA (do not let charger side terminate charge)     </br>\
3594                                 4. Wait 3,480 seconds (0:58 HR:MIN)                              </br>\
3595                                 5. Wait 240 seconds (1:02 HR:MIN)                                 </br>\
3596                                 6. Disable CE                                                     </br>\
3597                                 7. Wait 5 Seconds                                                 </br>\
3598                                 8. Enable CE                                                      </br>\
3599                                 9. Wait 5 seconds                                                 </br>\
3600                                 10. Attempt to charge at 2A                                            |
3601        | Pass / Fail Criteria | Pass IF at Step #...                                              </br>\
3602                                 1. SERIAL THERM1 and THERM2 are 23C +/- 1.1C                      </br>\
3603                                 2. SERIAL Battery 0 Voltage is 14.8V +/- 100 mV                   </br>\
3604                                 3. HITL Charge Current is 20 mA +70mA / -0mA                     </br>\
3605                                 4. SERIAL Battery 0 Voltage is 14.8V +/- 100 mV                   </br>\
3606                                 4. HITL Charge Current is 20 mA +70mA / -0mA                      </br>\
3607                                 4. SERIAL No Fault Flags                                          </br>\
3608                                 5. SERIAL Battery 0 Voltage is 14.8V +/- 100 mV                   </br>\
3609                                 5. HITL Charge Current is 0 mA +/- 1mA                            </br>\
3610                                 5. SERIAL Flag OverTime_Charge is flagged                         </br>\
3611                                 10. HITL Charge Current is 2A +/- 30mA                                 |
3612        | Estimated Duration   | 64 minutes                                                            |
3613        | Note                 | ??                                                                     |
3614        """
3615        serial_data = serial_monitor.read()
3616
3617        set_temp = 23
3618        # Check THERM 1 & Therm 2
3619        therm_one = serial_data["dk_temp"] / 10 - 273
3620        therm_two = serial_data["dk_temp1"] / 10 - 273
3621        low_range = set_temp - 1.1
3622        high_range = set_temp + 1.1
3623        temp_range = f"{set_temp}°C +/- 1.1°C"
3624
3625        if low_range <= therm_one <= high_range:
3626            logger.write_result_to_html_report(
3627                f"THERM1 was {therm_one:.1f}°C, which was within the expected range of {temp_range}"
3628            )
3629        else:
3630            logger.write_failure_to_html_report(
3631                f"THERM1 was {therm_one:.1f}°C, which was not within the expected range of {temp_range}"
3632            )
3633            pytest.fail()
3634
3635        if low_range <= therm_two <= high_range:
3636            logger.write_result_to_html_report(
3637                f"THERM2 was {therm_two:.1f}°C, which was within the expected range of {temp_range}"
3638            )
3639        else:
3640            logger.write_failure_to_html_report(
3641                f"THERM2 was {therm_two:.1f}°C, which was not within the expected range of {temp_range}"
3642            )
3643            pytest.fail()
3644
3645        # Check Serial Battery 0 Voltage:
3646        battery0_voltage_check(serial_data)
3647
3648        # Charge at 40 mA -- keep voltage at 3.7
3649        with _bms.charger(16.8, 0.040):
3650            logger.write_info_to_report("Charging at 40mA")
3651            time.sleep(60)
3652
3653            high_current_range = 0.020 + 0.070
3654            low_current_range = 0.020
3655
3656            expected_current_range = "20mA +70mA/-0mA"
3657            terminal_current = _bms.charger.amps
3658
3659            if low_current_range <= terminal_current <= high_current_range:
3660                logger.write_result_to_html_report(
3661                    f"HITL Terminal Current was {terminal_current:.3f}A after charging, which was within the expected "
3662                    f"range of {expected_current_range}"
3663                )
3664            else:
3665                logger.write_failure_to_html_report(
3666                    f"HITL Terminal Current was {terminal_current:.3f}A after charging, which was not within the "
3667                    f"expected range of {expected_current_range}"
3668                )
3669                pytest.fail()
3670
3671            # 1 hour (debug mode)
3672            long_rest = 3480
3673
3674            logger.write_info_to_report(f"Waiting for {long_rest} seconds")
3675            time.sleep(long_rest)
3676
3677            serial_data = serial_monitor.read()
3678
3679            # Check Battery 0 Voltage
3680            battery0_voltage_check(serial_data)
3681
3682            # Check HITL Charge Current
3683            terminal_current = _bms.charger.amps
3684
3685            if low_current_range <= terminal_current <= high_current_range:
3686                logger.write_result_to_html_report(
3687                    f"HITL Terminal Current was {terminal_current:.3f}A after waiting {long_rest} seconds, "
3688                    f"which was within the expected range of {expected_current_range}"
3689                )
3690            else:
3691                logger.write_failure_to_html_report(
3692                    f"HITL Terminal Current was {terminal_current:.3f}A after waiting {long_rest} seconds, "
3693                    f"which was not within the expected range of {expected_current_range}"
3694                )
3695                pytest.fail()
3696
3697            # Check Serial OverTime_Charge
3698            no_fault_flag = serial_data["flags.fault_overtime_charge"]
3699            if no_fault_flag is False:
3700                logger.write_result_to_html_report(
3701                    f"OverTime Charge Fault was False, the expected value after waiting {long_rest} seconds"
3702                )
3703            else:
3704                logger.write_failure_to_html_report(
3705                    f"OverTime Charge Fault was True, which was not expected after waiting {long_rest} seconds"
3706                )
3707                pytest.fail()
3708
3709            # Sleep for 240 seconds
3710            short_rest = 240
3711            logger.write_info_to_report(f"Waiting for {short_rest} seconds")
3712            time.sleep(short_rest)
3713
3714            serial_data = serial_monitor.read()
3715
3716            # Check Battery 0 Voltage
3717            battery0_voltage_check(serial_data)
3718
3719            # Check HITL Charge Current
3720            expected_charge = 0
3721            high_current_range = expected_charge + 0.020
3722            low_current_range = expected_charge - 0.020
3723
3724            expected_current_range = "0mA +/- 20mA"
3725            terminal_current = _bms.charger.amps
3726
3727            if low_current_range <= terminal_current <= high_current_range:
3728                logger.write_result_to_html_report(
3729                    f"HITL Terminal Current was {terminal_current:.3f}A after waiting {short_rest} seconds, "
3730                    f"which was within the expected range of {expected_current_range}"
3731                )
3732            else:
3733                logger.write_failure_to_html_report(
3734                    f"HITL Terminal Current was {terminal_current:.3f}A after waiting {short_rest} seconds, "
3735                    f"which was not within the expected range of {expected_current_range}"
3736                )
3737                pytest.fail()
3738
3739            # Check Overtime Charge Flag
3740            no_fault_flag = serial_data["flags.fault_overtime_charge"]
3741            if no_fault_flag is True:
3742                logger.write_result_to_html_report(
3743                    f"Overtime Charge Fault was True, the expected value after waiting {short_rest} seconds"
3744                )
3745            else:
3746                logger.write_failure_to_html_report(
3747                    f"Overtime Charge Fault was False, which was not expected after waiting {short_rest} seconds"
3748                )
3749                pytest.fail()
3750
3751        # Wait 5 Seconds (Since BMS_Charger is disabled)
3752        time.sleep(5)
3753
3754        # Enable BMS Charger & Wait 5 Seconds before attempting to charge
3755        with _bms.charger(16.8, 2):
3756            time.sleep(5)
3757
3758            # Check HITL Charge Current
3759            terminal_current = _bms.charger.amps
3760            expected_current = 2
3761            expected_current_range = "2A +/- 30mA"
3762            high_current_range = expected_current + 0.03
3763            low_current_range = expected_current
3764
3765            if low_current_range <= terminal_current <= high_current_range:
3766                logger.write_result_to_html_report(
3767                    f"HITL Terminal Current was {terminal_current:.3f}A after disabling/enabling charge, "
3768                    f"which was within the expected range of {expected_current_range}"
3769                )
3770            else:
3771                logger.write_failure_to_html_report(
3772                    f"HITL Terminal Current was {terminal_current:.3f}A after disabling/enabling charge, "
3773                    f"which was not within the expected range of {expected_current_range}"
3774                )
3775                pytest.fail()
3776
3777
3778class TestColdTemperatureCharging(CSVRecordEvent):
3779    """Run a test for cold charging."""
3780
3781    def test_cold_temperature_charging(self):
3782        """
3783        | Description          | Cold temperature charging                                              |
3784        | :------------------- | :--------------------------------------------------------------------- |
3785        | GitHub Issue         | turnaroundfactor/HITL#609                                              |
3786        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3787jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D42)                          |
3788        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
3789                                 2. Put cells in rested state at 3.7V per cell                     </br>\
3790                                 4. Attempt to charge at 3.2A                                      </br>\
3791                                 6. Set THERM1 and THERM2 to 0°C                                   </br>\
3792                                 7. Attempt to charge at 3.2A                                      </br>\
3793                                 7. Wait 5 seconds                                                 </br>\
3794                                 7. Disable charging                                               </br>\
3795                                 7. Wait 65 seconds                                                </br>\
3796                                 7. Set THERM1 and THERM2 to 7°C                                   </br>\
3797                                 8. Attempt to charge at 3.2A                                           |
3798        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 23°C +/- 1.1°C             </br>\
3799                                 ⦁ Expect HITL Terminal Current to be 3.2A +/- 30mA                </br>\
3800                                 ⦁ Expect Serial THERM1 & THERM 2 to be 0°C +/- 1.1°C              </br>\
3801                                 ⦁ Expect HITL Terminal Current to be 0A +/- 30mA                  </br>\
3802                                 ⦁ Expect Serial THERM1 & THERM 2 to be 7°C +/- 1.1°C              </br>\
3803                                 ⦁ Expect HITL Terminal Current to be 3.2A +/- 30mA                     |
3804        | Estimated Duration   | 17 seconds                                                             |
3805        """
3806
3807        failed_tests = []
3808        temperatures = [23, 0, 7]
3809
3810        for set_temp in temperatures:
3811            logger.write_info_to_report(f"Setting THERM1 & THERM2 to {set_temp}°C")
3812
3813            _plateset.disengage_safety_protocols = True
3814            _plateset.thermistor1 = _plateset.thermistor2 = set_temp
3815            _plateset.disengage_safety_protocols = False
3816
3817            time.sleep(2)
3818
3819            # Get the serial data
3820            serial_data = serial_monitor.read()
3821
3822            # Convert temperature to Celsius from Kelvin
3823            therm_one = serial_data["dk_temp"] / 10 - 273
3824            therm_two = serial_data["dk_temp1"] / 10 - 273
3825            temp_range = f"{set_temp}°C +/- 1.1°C"
3826            low_range = set_temp - 1.1
3827            high_range = set_temp + 1.1
3828
3829            if low_range <= therm_one <= high_range:
3830                logger.write_result_to_html_report(
3831                    f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
3832                )
3833            else:
3834                logger.write_result_to_html_report(
3835                    f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
3836                    f"of {temp_range}</font>"
3837                )
3838                failed_tests.append("THERM1")
3839
3840            if low_range <= therm_two <= high_range:
3841                logger.write_result_to_html_report(
3842                    f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
3843                )
3844            else:
3845                logger.write_result_to_html_report(
3846                    f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
3847                    f"expected range of {temp_range}</font>"
3848                )
3849                failed_tests.append("THERM2")
3850
3851            logger.write_info_to_report("Attempting to charge at 1A")
3852            limit = 0.030
3853            charge_current = 3.2
3854            expected_current_range = f"3.2A +/- {limit}A"
3855            with _bms.charger(16.8, charge_current):
3856                time.sleep(1)
3857                if set_temp == 0:
3858                    expected_current_range = f"0A +/- {limit}A"
3859                    charge_current = 0
3860                charger_amps = _bms.charger.amps
3861                if charge_current - limit <= charger_amps <= charge_current + limit:
3862                    logger.write_result_to_html_report(
3863                        f"HITL Terminal Current was {charger_amps:.3f}A after charging, which was within the "
3864                        f"expected range of {expected_current_range}"
3865                    )
3866                else:
3867                    logger.write_result_to_html_report(
3868                        f'<font color="#990000">HITL Terminal Current was {charger_amps:.3f}A after charging, '
3869                        f"which was not within the expected range of {expected_current_range} </font>"
3870                    )
3871                    failed_tests.append("HITL Terminal Current")
3872
3873                if set_temp == 0:
3874                    time.sleep(5)
3875            if set_temp == 0:
3876                time.sleep(65)
3877
3878        if len(failed_tests) > 0:
3879            pytest.fail()
3880
3881        logger.write_result_to_html_report("All checks passed test")
3882
3883
3884class TestColdTemperatureCurrent(CSVRecordEvent):
3885    """Run a test for cold current."""
3886
3887    def set_temperature(self, celsius: float) -> bool:
3888        """Set and check the temperature."""
3889        test_failed = False
3890
3891        logger.write_info_to_report(f"Setting THERM1 & THERM2 to {celsius}°C")
3892
3893        _plateset.disengage_safety_protocols = True
3894        _plateset.thermistor1 = _plateset.thermistor2 = celsius
3895        _plateset.disengage_safety_protocols = False
3896
3897        time.sleep(2)
3898
3899        # Get the serial data
3900        serial_data = serial_monitor.read()
3901        assert serial_data, "No serial data recieved."
3902
3903        # Convert temperature to Celsius from Kelvin
3904        therm_one = int(serial_data["dk_temp"]) / 10 - 273
3905        therm_two = int(serial_data["dk_temp1"]) / 10 - 273
3906        temp_range = f"{celsius}°C +/- 1.1°C"
3907        low_range = celsius - 1.1
3908        high_range = celsius + 1.1
3909
3910        if low_range <= therm_one <= high_range:
3911            logger.write_result_to_html_report(
3912                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
3913            )
3914        else:
3915            logger.write_result_to_html_report(
3916                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
3917                f"of {temp_range}</font>"
3918            )
3919            test_failed = True
3920
3921        if low_range <= therm_two <= high_range:
3922            logger.write_result_to_html_report(
3923                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
3924            )
3925        else:
3926            logger.write_result_to_html_report(
3927                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
3928                f"expected range of {temp_range}</font>"
3929            )
3930            test_failed = True
3931        return test_failed
3932
3933    def test_cold_temperature_current(self):
3934        """
3935        | Description          | Cold temperature current                                               |
3936        | :------------------- | :--------------------------------------------------------------------- |
3937        | GitHub Issue         | turnaroundfactor/HITL#609                                              |
3938        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3939jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D43)                          |
3940        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
3941                                 2. Put cells in rested state at 3.7V per cell                     </br>\
3942                                 3. Read SMBus charging current                                    </br>\
3943                                 4. Set THERM1 and THERM2 to 0°C                                   </br>\
3944                                 5. Read SMBus charging current                                         |
3945        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 23°C +/- 1.1°C             </br>\
3946                                 ⦁ Expect SMBus charging current 0x07D0 (2000 mA)                  </br>\
3947                                 ⦁ Expect Serial THERM1 & THERM 2 to be 0°C +/- 1.1°C              </br>\
3948                                 ⦁ Expect SMBus charging current 0x03EB (1000 mA)                       |
3949        | Estimated Duration   | 17 seconds                                                             |
3950        | Note                 | 0x07D0  (Note: This is the default value for debug build, Production   \
3951                                 build should return 0x1770)                                            |
3952        """
3953
3954        for temperature, expected_value in ((23, 2000), (0, 1000)):
3955            test_failed = self.set_temperature(temperature)
3956            time.sleep(5)
3957            charging_current = _smbus.read_register(SMBusReg.CHARGING_CURRENT)
3958            if charging_current[0] == expected_value:
3959                logger.write_result_to_html_report(f"Charging current: {charging_current[0]} = {expected_value}")
3960            else:
3961                logger.write_failure_to_html_report(f"Charging current: {charging_current[0]}{expected_value}")
3962                test_failed = True
3963
3964        if test_failed:
3965            pytest.fail()
FAST_MODE = False

Shorten test times if True.

CELL_CAPACITY_AH = 0

Cell capacity combined.

FLASH_SLEEP = 7

How long to wait for flash operations in seconds.

CELL_VOLTAGE = 3.8002

Default cell voltage.

@pytest.fixture(scope='function', autouse=True)
def reset_test_environment(request):
 53@pytest.fixture(scope="function", autouse=True)
 54def reset_test_environment(request):
 55    """
 56    Before each test, reset cell sims / BMS and set appropriate temperatures.
 57    After each test, clean up modified objects.
 58
 59    Fixture arguments are provided in an abnormal way, see below tests for details on how to provide these arguments.
 60    A default value is used if neither soc nor volts is provided.
 61
 62    :param float temperature: the initial temperature in C
 63    :param float soc: the initial state of charge
 64    :param float volts: the initial voltage
 65    """
 66    global CELL_CAPACITY_AH
 67
 68    request.param = getattr(request, "param", {})
 69
 70    # Reset cell sims
 71    starting_temperature = request.param.get("temperature", 23)
 72    if len(_bms.cells) > 0:
 73        logger.write_info_to_report(f"Setting temperature to {starting_temperature}°C")
 74        _plateset.thermistor1 = _plateset.thermistor2 = starting_temperature
 75
 76        logger.write_info_to_report("Powering down cell sims")
 77        for cell in _bms.cells.values():
 78            cell.disengage_safety_protocols = True
 79            cell.volts = 0.0001
 80        time.sleep(5)
 81
 82        for cell in _bms.cells.values():
 83            new_soc = request.param.get("soc") or cell.volts_to_soc(request.param.get("volts")) or 0.50
 84            logger.write_info_to_report(f"Powering up cell sim {cell.id} to {new_soc:%}")
 85            cell.state_of_charge = new_soc
 86            cell.disengage_safety_protocols = False
 87
 88        logger.write_info_to_report("Waiting 10 seconds for BMS...")
 89        time.sleep(10)
 90
 91        if not CELL_CAPACITY_AH and (serial_data := serial_monitor.read()):  # Get capacity from BMS
 92            CELL_CAPACITY_AH = float(serial_data["milliamp_hour_capacity"]) / 1000
 93        logger.write_info_to_report(f"Setting cell capacity to {CELL_CAPACITY_AH} Ah")
 94        for cell in _bms.cells.values():
 95            cell.data.capacity = CELL_CAPACITY_AH
 96
 97    # Clear permanent disables
 98    serial_monitor.read()  # Clear latest serial buffer
 99    serial_data = serial_monitor.read()
100    for key in serial_data:
101        if key.startswith(("flags.permanent", "flags.measure_output_fets_disabled")) and serial_data[key]:
102            # Erase flash
103            logger.write_warning_to_report("Detected permanent fault.")
104            logger.write_info_to_report("Erasing flash...")
105            _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.ERASE_FLASH)
106            time.sleep(FLASH_SLEEP)  # Wait for erase to complete
107            data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0]
108            logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}")
109
110            # Enable faults
111            logger.write_info_to_report("Enabling faults...")
112            try:
113                test_fault_enable()
114            except TimeoutError:
115                logger.write_error_to_report("Failed to enable faults.")
116
117            # Recalibrate
118            logger.write_info_to_report("Recalibrating...")
119            try:
120                TestCalibration().test_calibration()
121            except AssertionError:
122                logger.write_error_to_report("Failed to calibrate.")
123            break
124
125    CSVRecordEvent.current_test(request.cls)  # Automatically register any tests defined in this class
126
127    yield  # Run test
128
129    CSVRecordEvent.failed()  # Record results regardless of failure or success
130    CSVRecordEvent.clear_tests()

Before each test, reset cell sims / BMS and set appropriate temperatures. After each test, clean up modified objects.

Fixture arguments are provided in an abnormal way, see below tests for details on how to provide these arguments. A default value is used if neither soc nor volts is provided.

Parameters
  • float temperature: the initial temperature in C
  • float soc: the initial state of charge
  • float volts: the initial voltage
def standard_charge( charge_current: float = 2, max_time: int = 28800, sample_interval: int = 10, minimum_readings: int = 3, termination_current: float = 0.1):
133def standard_charge(
134    charge_current: float = 2,
135    max_time: int = 8 * 3600,
136    sample_interval: int = 10,
137    minimum_readings: int = 3,
138    termination_current: float = 0.100,
139):
140    """
141    Helper function to charge batteries in accordance with 4.3.1 for not greater than three hours.
142    4.3.1 = 23 ± 5°C (73.4°F) ambient pressure/relative humidity, with 2+ hours between charge and discharge.
143    """
144    _bms.voltage = 16.8
145    _bms.ov_protection = _bms.voltage + 0.050  # 50mV above the charging voltage
146    _bms.current = charge_current
147    _bms.termination_current = termination_current  # 100 mA
148    _bms.max_time = max_time
149    _bms.sample_interval = sample_interval
150    _bms.minimum_readings = minimum_readings
151
152    # Run the Charge cycle
153    _plateset.ce_switch = True
154    _bms.run_li_charge_cycle()
155    _plateset.ce_switch = False

Helper function to charge batteries in accordance with 4.3.1 for not greater than three hours. 4.3.1 = 23 ± 5°C (73.4°F) ambient pressure/relative humidity, with 2+ hours between charge and discharge.

def standard_rest(seconds: float = 7200, sample_interval: int = 10):
158def standard_rest(seconds: float = 2 * 3600, sample_interval: int = 10):
159    """Helper function to stabilize the batteries for 2+ hours."""
160    _bms.max_time = seconds
161    _bms.sample_interval = sample_interval
162    _bms.run_resting_cycle()

Helper function to stabilize the batteries for 2+ hours.

def standard_discharge( discharge_current: float = 2, max_time: int = 28800, sample_interval: int = 10, discharge_voltage: float = 10):
165def standard_discharge(
166    discharge_current: float = 2, max_time: int = 8 * 3600, sample_interval: int = 10, discharge_voltage: float = 10
167):
168    """Helper function to discharge at 2A until 10V."""
169    _bms.voltage = discharge_voltage
170    _bms.uv_protection = _bms.voltage - 0.500  # 500mV below voltage cutoff
171    _bms.current = discharge_current
172    _bms.discharge_type = DischargeType.CONSTANT_CURRENT
173    _bms.max_time = max_time
174    _bms.sample_interval = sample_interval
175
176    # Run the discharge cycle, returning the capacity
177    capacity = _bms.run_discharge_cycle()
178    logger.write_info_to_report(f"Discharge complete, capacity was {capacity * 1000.0} mAh")
179    return capacity

Helper function to discharge at 2A until 10V.

def test_fault_enable():
182def test_fault_enable():
183    """
184    | Description          | Enable faults via SMBus.                                               |
185    | :------------------- | :--------------------------------------------------------------------- |
186    | GitHub Issue         | turnaroundfactor/HITL#476                                       |
187    | Instructions         | 1. Enable faults via SMBus                                        </br>\
188                             2. Raise an over-temp fault                                       </br>\
189                             3. Clear the over-temp fault                                           |
190    | Pass / Fail Criteria | Pass if faults can be raised                                           |
191    | Estimated Duration   | 1 minute                                                               |
192    """
193
194    _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.FAULT_ENABLE)
195    time.sleep(FLASH_SLEEP)
196
197    # Check if overtemp faults can be raised.
198    timeout_s = 30
199
200    # Raise a fault
201    _plateset.thermistor1 = 65
202    start = time.perf_counter()
203    while (serial_data := serial_monitor.read(latest=True)) and not serial_data["flags.fault_overtemp_discharge"]:
204        if time.perf_counter() - start > timeout_s:
205            message = f"Over-temperature fault was not raised after {timeout_s} seconds."
206            logger.write_failure_to_html_report(message)
207            raise TimeoutError(message)
208    logger.write_result_to_html_report("Fault successfully raised.")
209
210    # Clear the fault
211    _plateset.thermistor1 = 45
212    start = time.perf_counter()
213    while (serial_data := serial_monitor.read(latest=True)) and serial_data["flags.fault_overtemp_discharge"]:
214        if time.perf_counter() - start > timeout_s:
215            message = f"Over-temperature fault was not cleared after {timeout_s} seconds."
216            logger.write_failure_to_html_report(message)
217            raise TimeoutError(message)
218    logger.write_result_to_html_report("Fault successfully cleared.")
Description Enable faults via SMBus.
GitHub Issue turnaroundfactor/HITL#476
Instructions 1. Enable faults via SMBus
2. Raise an over-temp fault
3. Clear the over-temp fault
Pass / Fail Criteria Pass if faults can be raised
Estimated Duration 1 minute
class TestCalibration:
221class TestCalibration:
222    """Calibrate the BMS if needed."""
223
224    average = 0
225    readings = 0
226
227    def bms_current(self):
228        """Measure serial current and calculate an average."""
229        assert (serial_date := serial_monitor.read()), "Could not read serial."
230        new_reading = serial_date["mamps"]
231        self.average = (new_reading + self.readings * self.average) / (self.readings + 1)
232        self.readings += 1
233        logger.write_info_to_report(f"BMS Serial Current (mA): {new_reading:.3f}")  # Output current on every sample
234
235    def test_calibration(self):
236        """
237        | Description          | Test calibration retention                                             |
238        | :------------------- | :--------------------------------------------------------------------- |
239        | GitHub Issue         | turnaroundfactor/HITL#413                                       |
240        | Instructions         | 1. Calibrate the BMS                                              </br>\
241                                 2. Confirm BMS is calibrated                                           |
242        | Pass / Fail Criteria | Pass if calibrated                                                     |
243        | Estimated Duration   | 1 minute                                                               |
244        """
245        acceptable_error_ma = 5
246        scan_count = 6  # How many measurements to take for an average.
247
248        self.readings = 0  # Reset average
249        for _ in range(scan_count):  # Measure current over some period
250            self.bms_current()
251            time.sleep(5)
252        logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA")
253        offset = int(round(self.average, 0))
254        logger.write_info_to_report(f"Setting offset current to {offset:.3f} mA")
255        data = (ctypes.c_uint8(offset).value << 8) | BMSCommands.CALIBRATE
256        _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, data)
257        time.sleep(FLASH_SLEEP)
258        data = cast(int, _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0])
259        logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}")
260
261        # Confirm current is in acceptable range
262        self.readings = 0  # Reset average
263        for _ in range(scan_count):
264            self.bms_current()
265            time.sleep(5)
266        logger.write_result_to_html_report(f"Average rest current (calibrated): {self.average:.3f} mA")
267        assert (
268            acceptable_error_ma > self.average > -acceptable_error_ma
269        ), f"{self.average:.3f} mA outside limit of ±{acceptable_error_ma:.3f} mA"

Calibrate the BMS if needed.

average = 0
readings = 0
def bms_current(self):
227    def bms_current(self):
228        """Measure serial current and calculate an average."""
229        assert (serial_date := serial_monitor.read()), "Could not read serial."
230        new_reading = serial_date["mamps"]
231        self.average = (new_reading + self.readings * self.average) / (self.readings + 1)
232        self.readings += 1
233        logger.write_info_to_report(f"BMS Serial Current (mA): {new_reading:.3f}")  # Output current on every sample

Measure serial current and calculate an average.

def test_calibration(self):
235    def test_calibration(self):
236        """
237        | Description          | Test calibration retention                                             |
238        | :------------------- | :--------------------------------------------------------------------- |
239        | GitHub Issue         | turnaroundfactor/HITL#413                                       |
240        | Instructions         | 1. Calibrate the BMS                                              </br>\
241                                 2. Confirm BMS is calibrated                                           |
242        | Pass / Fail Criteria | Pass if calibrated                                                     |
243        | Estimated Duration   | 1 minute                                                               |
244        """
245        acceptable_error_ma = 5
246        scan_count = 6  # How many measurements to take for an average.
247
248        self.readings = 0  # Reset average
249        for _ in range(scan_count):  # Measure current over some period
250            self.bms_current()
251            time.sleep(5)
252        logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA")
253        offset = int(round(self.average, 0))
254        logger.write_info_to_report(f"Setting offset current to {offset:.3f} mA")
255        data = (ctypes.c_uint8(offset).value << 8) | BMSCommands.CALIBRATE
256        _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, data)
257        time.sleep(FLASH_SLEEP)
258        data = cast(int, _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0])
259        logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}")
260
261        # Confirm current is in acceptable range
262        self.readings = 0  # Reset average
263        for _ in range(scan_count):
264            self.bms_current()
265            time.sleep(5)
266        logger.write_result_to_html_report(f"Average rest current (calibrated): {self.average:.3f} mA")
267        assert (
268            acceptable_error_ma > self.average > -acceptable_error_ma
269        ), f"{self.average:.3f} mA outside limit of ±{acceptable_error_ma:.3f} mA"
Description Test calibration retention
GitHub Issue turnaroundfactor/HITL#413
Instructions 1. Calibrate the BMS
2. Confirm BMS is calibrated
Pass / Fail Criteria Pass if calibrated
Estimated Duration 1 minute
def test_charge_enable_on():
272def test_charge_enable_on():
273    """
274    | Description          | Confirm charging above 400mA works when CE is active             |
275    | :------------------- | :--------------------------------------------------------------- |
276    | GitHub Issue         | turnaroundfactor/HITL#342                                 |
277    | MIL-PRF Section      | 3.5.6.2 (Charge Enable)                                          |
278    | MIL-PRF Requirements | The charge enable terminal shall comply with the following: </br>\
279                              ⠀⠀a. Maximum charge without enable: 400 mA                 </br>\
280                              ⠀⠀b. Equivalent resistor: 235 Ω                            </br>\
281                              ⠀⠀c. Equivalent diode VF: 1.3 V                            </br>\
282                              ⠀⠀d. Approximate activation current: 7 mA                       |
283    | Instructions         | 1. Activate CE                                               </br>\
284                             2. Charge at 2A                                                  |
285    | Pass / Fail Criteria | Pass if current is more than 400mA                               |
286    | Estimated Duration   | 1 minute                                                         |
287    | Note                 | This test can fail if the battery is sufficiently charged.       |
288    """
289    passing_current = 0.400
290
291    # Enable charging and timer
292    _plateset.ce_switch = True
293    _bms.charger.set_profile(volts=16.8, amps=2)
294    _bms.charger.enable()
295    _bms.timer.reset()  # Keep track of runtime
296
297    # Charge until current is more than passing_current or timeout
298    timeout_seconds = 10
299    while (latest_current := _bms.charger.amps) < passing_current and _bms.timer.elapsed_time <= timeout_seconds:
300        logger.write_info_to_report(
301            f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Voltage: {_bms.dmm.volts:.3f}, "
302            f"Current: {latest_current:.3f}"
303        )
304        time.sleep(1)
305
306    # Charging is complete, turn off the charger
307    _bms.charger.disable()
308    _plateset.ce_switch = False
309
310    # Check results
311    logger.write_result_to_html_report(f"Current: {latest_current:.3f} A ≥ {passing_current:.3f} A")
312    if latest_current < passing_current:
313        pytest.fail(f"Current of {latest_current:.3f} A does not exceed {passing_current:.3f} A.")
Description Confirm charging above 400mA works when CE is active
GitHub Issue turnaroundfactor/HITL#342
MIL-PRF Section 3.5.6.2 (Charge Enable)
MIL-PRF Requirements The charge enable terminal shall comply with the following:
⠀⠀a. Maximum charge without enable: 400 mA
⠀⠀b. Equivalent resistor: 235 Ω
⠀⠀c. Equivalent diode VF: 1.3 V
⠀⠀d. Approximate activation current: 7 mA
Instructions 1. Activate CE
2. Charge at 2A
Pass / Fail Criteria Pass if current is more than 400mA
Estimated Duration 1 minute
Note This test can fail if the battery is sufficiently charged.
def test_charge_enable_off():
316def test_charge_enable_off():
317    """
318    | Description          | Confirm charging above 400mA doesn't work when CE is inactive          |
319    | :------------------- | :--------------------------------------------------------------------- |
320    | GitHub Issue         | turnaroundfactor/HITL#342                                       |
321    | MIL-PRF Section      | 3.5.6.2 (Charge Enable)                                                |
322    | MIL-PRF Requirements | The charge enable terminal shall comply with the following:       </br>\
323                              ⠀⠀a. Maximum charge without enable: 400 mA                       </br>\
324                              ⠀⠀b. Equivalent resistor: 235 Ω                                  </br>\
325                              ⠀⠀c. Equivalent diode VF: 1.3 V                                  </br>\
326                              ⠀⠀d. Approximate activation current: 7 mA                             |
327    | Instructions         | 1. Increment charge current every second                          </br>\
328                             2. Stop when charge current drops to ~0A                               |
329    | Pass / Fail Criteria | Pass if the highest current is less than 400mA                         |
330    | Estimated Duration   | 5 minutes                                                              |
331    """
332    failing_current = 0.400
333    uncertainty = 0.005
334
335    # Enable charging and timer
336    _plateset.ce_switch = False
337    _bms.charger.set_profile(volts=16.8, amps=0.010)  # Starting current
338    _bms.charger.enable()
339    _bms.timer.reset()  # Keep track of runtime
340
341    # Charge until current drops below some threshold (can float above 0) or is more than failing_current
342    max_charge_current = 0
343    while (
344        _bms.charger.target_amps <= failing_current + uncertainty
345        and abs(_bms.charger.target_amps - (latest_current := _bms.charger.amps)) <= uncertainty
346    ):
347        # while (failing_current + uncertainty * 2) > (latest_current := _bms.charger.amps) > 0.005:
348        max_charge_current = max(max_charge_current, latest_current)
349        logger.write_info_to_report(
350            f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Voltage: {_bms.dmm.volts:.3f}, "
351            f"Current: {latest_current:.3f}"
352        )
353        # Increase by 1 mA / second
354        _bms.charger.set_profile(volts=_bms.charger.target_volts, amps=_bms.charger.target_amps + 0.001)
355        time.sleep(1)
356
357    # Charging is complete, turn off the charger
358    _bms.charger.disable()
359
360    # Check results
361    logger.write_result_to_html_report(f"Current: {max_charge_current:.3f} A ≤ {failing_current:.3f} ± {uncertainty} A")
362    if max_charge_current > failing_current + uncertainty:
363        pytest.fail(f"Current of {max_charge_current:.3f} A exceeds {failing_current:.3f} A limit.")
Description Confirm charging above 400mA doesn't work when CE is inactive
GitHub Issue turnaroundfactor/HITL#342
MIL-PRF Section 3.5.6.2 (Charge Enable)
MIL-PRF Requirements The charge enable terminal shall comply with the following:
⠀⠀a. Maximum charge without enable: 400 mA
⠀⠀b. Equivalent resistor: 235 Ω
⠀⠀c. Equivalent diode VF: 1.3 V
⠀⠀d. Approximate activation current: 7 mA
Instructions 1. Increment charge current every second
2. Stop when charge current drops to ~0A
Pass / Fail Criteria Pass if the highest current is less than 400mA
Estimated Duration 5 minutes
def test_taf_charge_enable_off():
366def test_taf_charge_enable_off():
367    """
368    | Description          | TAF: Confirm charging above 20mA doesn't work when CE is inactive      |
369    | :------------------- | :--------------------------------------------------------------------- |
370    | GitHub Issue         | turnaroundfactor/HITL#342                                       |
371    | MIL-PRF Section      | 3.5.6.2 (Charge Enable)                                                |
372    | Instructions         | 1. Increment charge current every second                          </br>\
373                             2. Stop when charge current drops to ~0A                               |
374    | Pass / Fail Criteria | Pass if the highest current is less than or equal to 20mA              |
375    | Estimated Duration   | 1 minute                                                               |
376    | Note                 | We want to disable if current is over 20 mA since                      \
377                             that's what BT does, and it's a safer way to charge. Other OTS         \
378                             may have different cutoffs, and the BT one was measured by experiment. |
379    """
380    target_current = 0.020
381    uncertainty = 0.005
382
383    # Enable charging and timer
384    _plateset.ce_switch = False
385    _bms.charger.set_profile(volts=16.8, amps=0.010)  # Starting current
386    _bms.charger.enable()
387    _bms.timer.reset()  # Keep track of runtime
388
389    # Charge until current drops below some threshold (can float above 0) or is more than failing_current
390    max_charge_current = 0
391    while (
392        _bms.charger.target_amps <= target_current + uncertainty
393        and abs(_bms.charger.target_amps - (latest_current := _bms.charger.amps)) <= uncertainty
394    ):
395        max_charge_current = max(max_charge_current, latest_current)
396        logger.write_info_to_report(
397            f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Voltage: {_bms.dmm.volts:.3f}, "
398            f"Current: {latest_current:.3f}"
399        )
400        # Increase by 1 mA / second
401        _bms.charger.set_profile(volts=_bms.charger.target_volts, amps=_bms.charger.target_amps + 0.001)
402        time.sleep(1)
403
404    # Charging is complete, turn off the charger
405    _bms.charger.disable()
406
407    # Check results
408    logger.write_result_to_html_report(f"Current: {max_charge_current:.3f} A = {target_current:.3f} ± {uncertainty} A")
409    if not target_current - uncertainty <= max_charge_current <= target_current + uncertainty:
410        pytest.fail(f"Current of {max_charge_current:.3f} A exceeds {target_current:.3f} A limit.")
Description TAF: Confirm charging above 20mA doesn't work when CE is inactive
GitHub Issue turnaroundfactor/HITL#342
MIL-PRF Section 3.5.6.2 (Charge Enable)
Instructions 1. Increment charge current every second
2. Stop when charge current drops to ~0A
Pass / Fail Criteria Pass if the highest current is less than or equal to 20mA
Estimated Duration 1 minute
Note We want to disable if current is over 20 mA since that's what BT does, and it's a safer way to charge. Other OTS may have different cutoffs, and the BT one was measured by experiment.
@pytest.mark.parametrize('reset_test_environment', [{'volts': 2.5}], indirect=True)
class TestExtendedCycle:
413@pytest.mark.parametrize("reset_test_environment", [{"volts": 2.5}], indirect=True)
414class TestExtendedCycle:
415    """Perform a long charge / discharge."""
416
417    class CellVoltageDiscrepancy(CSVRecordEvent):
418        """@private Compare cell sim voltage to reported cell voltage."""
419
420        allowable_error = 0.01
421        max = SimpleNamespace(cell_id=0, sim_v=0, bms_v=0, error=0.0)
422
423        @classmethod
424        def failed(cls) -> bool:
425            """Check if test parameters were exceeded."""
426            return bool(cls.max.error > cls.allowable_error)
427
428        @classmethod
429        def verify(cls, row, serial_data, _cell_data):
430            """Cell voltage within range"""
431            for i, cell_id in enumerate(_bms.cells):
432                row_data = SimpleNamespace(
433                    cell_id=cell_id,
434                    sim_v=row[f"ADC Plate Cell {cell_id} Voltage (V)"],
435                    bms_v=serial_data[f"mvolt_cell{'' if i == 0 else i}"] / 1000,
436                )
437                row_data.error = abs((row_data.bms_v - row_data.sim_v) / row_data.sim_v)
438                cls.max = max(cls.max, row_data, key=lambda data: data.error)
439
440        @classmethod
441        def result(cls):
442            """Detailed test result information."""
443            return (
444                f"Cell Voltage error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)} "
445                f"(Sim {cls.max.cell_id}: {cls.max.sim_v * 1000:.1f} mv, BMS: {cls.max.bms_v * 1000:.1f} mv)"
446            )
447
448    class CurrentDiscrepancy(CSVRecordEvent):
449        """@private Compare terminal current to reported current."""
450
451        allowable_error = 0.015
452        max = SimpleNamespace(hitl_a=0, bms_a=0, error=0.0)
453
454        @classmethod
455        def failed(cls) -> bool:
456            """Check if test parameters were exceeded."""
457            return bool(cls.max.error > cls.allowable_error)
458
459        @classmethod
460        def verify(cls, row, serial_data, _cell_data):
461            """Current within range"""
462            row_data = SimpleNamespace(hitl_a=row["HITL Current (A)"], bms_a=serial_data["mamps"] / 1000)
463            row_data.error = abs((row_data.bms_a - row_data.hitl_a) / row_data.hitl_a)
464            if abs(row_data.hitl_a) > 0.100:  # Ignore currents within 100mA to -100mA
465                cls.max = max(cls.max, row_data, key=lambda data: data.error)
466
467        @classmethod
468        def result(cls):
469            """Detailed test result information."""
470            return (
471                f"Current error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)} "
472                f"(HITL: {cls.max.hitl_a * 1000:.1f} mA, BMS: {cls.max.bms_a * 1000:.1f} mA)"
473            )
474
475    class TerminalVoltageDiscrepancy(CSVRecordEvent):
476        """@private Compare HITL voltage to reported Terminal voltage."""
477
478        allowable_error = 0.015
479        max = SimpleNamespace(hitl_v=0, bms_v=0, error=0.0)
480
481        @classmethod
482        def failed(cls) -> bool:
483            """Check if test parameters were exceeded."""
484            return bool(cls.max.error > cls.allowable_error)
485
486        @classmethod
487        def verify(cls, row, serial_data, _cell_data):
488            """Terminal voltage within range"""
489            row_data = SimpleNamespace(hitl_v=row["HITL Voltage (V)"], bms_v=serial_data["mvolt_terminal"] / 1000)
490            row_data.error = abs((row_data.bms_v - row_data.hitl_v) / row_data.hitl_v)
491            cls.max = max(cls.max, row_data, key=lambda data: data.error)
492
493        @classmethod
494        def result(cls):
495            """Detailed test result information."""
496            return (
497                f"Terminal Voltage error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)} "
498                f"(HITL: {cls.max.hitl_v * 1000:.1f} mv, BMS: {cls.max.bms_v * 1000:.1f} mv)"
499            )
500
501    class TemperatureDiscrepancyTherm1(CSVRecordEvent):
502        """@private Compare HITL temperature to reported temperature."""
503
504        allowable_error = 5.0
505        max = SimpleNamespace(hitl_c=0, bms_c=0, error=0.0)
506
507        @classmethod
508        def failed(cls) -> bool:
509            """Check if test parameters were exceeded."""
510            return bool(cls.max.error > cls.allowable_error)
511
512        @classmethod
513        def verify(cls, _row, serial_data, _cell_data):
514            """Temperature within range"""
515            row_data = SimpleNamespace(hitl_c=_plateset.thermistor1, bms_c=serial_data["dk_temp"] / 10 - 273)
516            row_data.error = abs(row_data.bms_c - row_data.hitl_c)
517            cls.max = max(cls.max, row_data, key=lambda data: data.error)
518
519        @classmethod
520        def result(cls):
521            """Detailed test result information."""
522            return (
523                f"Thermistor 1 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error, '°C', '.2f')}"
524                f"(HITL: {cls.max.hitl_c:.2f} °C, BMS: {cls.max.bms_c:.2f} °C)"
525            )
526
527    class TemperatureDiscrepancyTherm2(CSVRecordEvent):
528        """@private Compare HITL temperature to reported temperature."""
529
530        allowable_error = 5.0
531        max = SimpleNamespace(hitl_c=0, bms_c=0, error=0.0)
532
533        @classmethod
534        def failed(cls) -> bool:
535            """Check if test parameters were exceeded."""
536            return bool(cls.max.error > cls.allowable_error)
537
538        @classmethod
539        def verify(cls, _row, serial_data, _cell_data):
540            """Temperature within range"""
541            row_data = SimpleNamespace(hitl_c=_plateset.thermistor2, bms_c=serial_data["dk_temp1"] / 10 - 273)
542            row_data.error = abs(row_data.bms_c - row_data.hitl_c)
543            cls.max = max(cls.max, row_data, key=lambda data: data.error)
544
545        @classmethod
546        def result(cls):
547            """Detailed test result information."""
548            return (
549                f"Thermistor 2 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error, '°C', '.2f')}"
550                f"(HITL: {cls.max.hitl_c:.2f} °C, BMS: {cls.max.bms_c:.2f} °C)"
551            )
552
553    class NoFaults(CSVRecordEvent):
554        """@private Check for any faults."""
555
556        faults: list[str] = []
557
558        @classmethod
559        def failed(cls) -> bool:
560            """Check if test parameters were exceeded."""
561            return len(cls.faults) != 0
562
563        @classmethod
564        def verify(cls, _row, serial_data, _cell_data):
565            """Check for faults."""
566            for key in serial_data:
567                if "fault" in key and key.startswith("flags.") and serial_data[key]:
568                    cls.faults.append(key.removeprefix("flags.").title())
569
570        @classmethod
571        def result(cls):
572            """Detailed test result information."""
573            return f"Faults encountered: {' | '.join(cls.faults) or None}"
574
575    class NoReset(CSVRecordEvent):
576        """@private Check for resets."""
577
578        reset_reasons = ControlStatusRegister(0)
579
580        @classmethod
581        def failed(cls) -> bool:
582            """Check if test parameters were exceeded."""
583            return bool(cls.reset_reasons)
584
585        @classmethod
586        def verify(cls, _row, serial_data, _cell_data):
587            """Check if any resets occurred."""
588            reset_reason = ControlStatusRegister(serial_data.get("Reset_Flags", 0))
589            reset_reason &= ~ControlStatusRegister.POWER & ~ControlStatusRegister.RESET_PIN  # Ignore valid reasons
590            cls.reset_reasons |= reset_reason
591
592        @classmethod
593        def result(cls):
594            """Detailed test result information."""
595            return f"Resets encountered: {str(cls.reset_reasons) or None}"
596
597    class SOCDiscrepancy(CSVRecordEvent):
598        """@private Compare lowest HITL cell SOC to reported SOC."""
599
600        allowable_error = 0.05
601        max = SimpleNamespace(sim_id=0, sim_soc=0, bms_soc=0, error=0.0)
602
603        @classmethod
604        def failed(cls) -> bool:
605            """Check if test parameters were exceeded."""
606            return bool(cls.max.error > cls.allowable_error)
607
608        @classmethod
609        def verify(cls, _row, serial_data, cell_data):
610            """SOC within range."""
611            lowest_sim_soc_id = min(cell_data, key=lambda cell_id: cell_data[cell_id]["state_of_charge"])
612            lowest_sim_soc = cell_data[lowest_sim_soc_id]["state_of_charge"]
613            row_data = SimpleNamespace(
614                sim_id=lowest_sim_soc_id, sim_soc=lowest_sim_soc, bms_soc=serial_data["percent_charged"] / 100
615            )
616            row_data.error = abs(row_data.bms_soc - row_data.sim_soc)
617            cls.max = max(cls.max, row_data, key=lambda data: data.error)
618
619        @classmethod
620        def result(cls):
621            """Detailed test result information."""
622            return (
623                f"SOC error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)} "
624                f"(Sim {cls.max.sim_id}: {cls.max.sim_soc:.1%}, BMS: {cls.max.bms_soc:.1%})"
625            )
626
627    class HealthChangeCount(CSVRecordEvent):
628        """@private Check how many times Health changes."""
629
630        changes = 0
631        allowable_changes = 1
632        last_reported_health: int | None = None
633
634        @classmethod
635        def failed(cls) -> bool:
636            """Check if test parameters were exceeded."""
637            return bool(cls.changes > cls.allowable_changes)
638
639        @classmethod
640        def verify(cls, _row, serial_data, _cell_data):
641            """Detect health change."""
642            if serial_data["percent_health"] != cls.last_reported_health:
643                cls.changes += cls.last_reported_health is not None
644                cls.last_reported_health = serial_data["percent_health"]
645
646        @classmethod
647        def result(cls):
648            """Detailed test result information."""
649            return f"Health change#: {cls.cmp(cls.changes, '<=', cls.allowable_changes, form='d')}"
650
651    class HealthChange(CSVRecordEvent):
652        """@private Check health change."""
653
654        allowable_change = 0.01
655        max_change: float = 0.0
656        initial_health: float | None = None
657
658        @classmethod
659        def failed(cls) -> bool:
660            """Check if test parameters were exceeded."""
661            return bool(cls.max_change > cls.allowable_change)
662
663        @classmethod
664        def verify(cls, _row, serial_data, _cell_data):
665            """Health change within range."""
666            if cls.initial_health is None:
667                cls.initial_health = serial_data["percent_health"] / 100
668            cls.max_change = max(cls.max_change, abs(cls.initial_health - serial_data["percent_health"] / 100))
669
670        @classmethod
671        def result(cls):
672            """Detailed test result information."""
673            return f"Health change: {cls.cmp(cls.max_change, '<=', cls.allowable_change)}"
674
675    class UsedAhDiscrepancy(CSVRecordEvent):
676        """@private Compare HITL used Ah to reported used Ah."""
677
678        allowable_error = 0.01
679        max = SimpleNamespace(hitl_ah=0, bms_ah=0, error=0.0)
680        initial_charge_cycle: int | None = None
681
682        @classmethod
683        def failed(cls) -> bool:
684            """Check if test parameters were exceeded."""
685            return bool(cls.max.error > cls.allowable_error)
686
687        @classmethod
688        def verify(cls, row, serial_data, _cell_data):
689            """Check used Ah during discharge."""
690            if cls.initial_charge_cycle is None:
691                cls.initial_charge_cycle = serial_data["charge_cycles"]
692            delta_cycle = serial_data["charge_cycles"] - cls.initial_charge_cycle
693            row_data = SimpleNamespace(
694                hitl_ah=-(row["HITL Capacity (Ah)"] or 0),
695                bms_ah=(serial_data["milliamp_hour_used"] + delta_cycle * serial_data["milliamp_hour_capacity"]) / 1000,
696            )
697            if row["Cycle"] == "run_discharge_cycle" and row_data.hitl_ah > 0.100:
698                logger.write_debug_to_report(f"  In discharge: {row_data.hitl_ah} Ah")
699                row_data.error = abs((row_data.bms_ah - row_data.hitl_ah) / row_data.hitl_ah)
700                cls.max = max(cls.max, row_data, key=lambda data: data.error)
701
702        @classmethod
703        def result(cls):
704            """Detailed test result information."""
705            return (
706                f"Used Ah error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)} "
707                f"(HITL: {cls.max.hitl_ah * 1000:.1f} mAh, BMS: {cls.max.bms_ah * 1000:.1f} mAh)"
708            )
709
710    class ChargeCycle(CSVRecordEvent):
711        """@private Compare cell sim voltage to reported cell voltage."""
712
713        allowable_cycles = 1
714        initial_cycle: int | None = None
715        max_cycle = 0
716
717        @classmethod
718        def failed(cls) -> bool:
719            """Check if test parameters were exceeded."""
720            return cls.initial_cycle is None or bool(cls.max_cycle - cls.initial_cycle > cls.allowable_cycles)
721
722        @classmethod
723        def verify(cls, _row, serial_data, _cell_data):
724            """Cell voltage within range"""
725            if cls.initial_cycle is None:
726                cls.initial_cycle = serial_data["charge_cycles"]
727            cls.max_cycle = max(cls.max_cycle, serial_data["charge_cycles"])
728
729        @classmethod
730        def result(cls):
731            """Detailed test result information."""
732            return (
733                f"Cycle change: "
734                f"{cls.cmp(cls.max_cycle - (cls.initial_cycle or 0), '<=', cls.allowable_cycles, form='d')} "
735                f"(Starting: {cls.initial_cycle}, Ending: {cls.max_cycle})"
736            )
737
738    def test_extended_cycle(self):
739        """
740        | Description          | Perform a long charge / discharge                                      |
741        | :------------------- | :--------------------------------------------------------------------- |
742        | GitHub Issue         | turnaroundfactor/HITL#342                                       |
743        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
744jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
745        | MIL-PRF Sections     | 4.7.2.3 (Capacity discharge)                                      </br>\
746                                 4.6.1 (Standard Charge)                                           </br>\
747                                 4.3.1 (Normal conditions)                                         </br>\
748                                 3.5.3 (Capacity)                                                       |
749        | Instructions         | 1. Set thermistors to 23C                                         </br>\
750                                 2. Put cells in a rested state at 2.5V per cell                   </br>\
751                                 3. Charge battery (16.8V / 3A / 100 mA cutoff)                    </br>\
752                                 4. Rest for 3.1 hours (we want to calculate SoH change which requires 3 hours) </br>\
753                                 5. Discharge at 2A until battery reaches 10V then stop discharging     |
754        | Pass / Fail Criteria | ⦁ Serial cell voltage agrees with HITL cell voltage to within 1%  </br>\
755                                 ⦁ Serial terminal current agrees with HITL current to within 1%   </br>\
756                                 ⦁ Serial terminal voltage agrees with HITL voltage to within 1%   </br>\
757                                 ⦁ Serial thermistor 1 and 2 agree with HITL thermistor 1 and 2 to within 5°C </br>\
758                                 ⦁ No Fault Flags over entire duration of test                     </br>\
759                                 ⦁ No resets occur over entire duration of test                    </br>\
760                                 ⦁ Serial cell SOC agrees with lowest HITL cell SOC to within 5% SOC </br>\
761                                 ⦁ Serial Health shall only change once                            </br>\
762                                 ⦁ Serial Health shall not change by more than 1%                  </br>\
763                                 ⦁ Serial used Ah agrees with HITL Ah to within 1% (for abs(HITL Ah > 100mAh))  </br>\
764                                 ⦁ Serial charge cycle shall only increment once                   </br>\
765                                 ⦁ E-ink display is operational [TBD How to do this]                    |
766        | Estimated Duration   | 12 hours                                                               |
767        | Note                 | MIL-PRF 4.7.2.3.1 (Initial capacity discharge): Each battery subjected \
768                                 to the capacity discharge test above (see 4.7.2.3) on its initial      \
769                                 charge/discharge cycle is permitted up to three cycles to meet the     \
770                                 capacity discharge test requirement (see 3.1). Any battery not meeting \
771                                 the specified capacity discharge requirement (see 3.1) during any of   \
772                                 its first three cycles is considered a failure                         |
773        """
774        # FIXME(JA): adjust estimated duration based on first test
775
776        standard_charge()
777        standard_rest(seconds=30 if FAST_MODE else 3.1 * 3600)
778        standard_discharge()
779
780        # Check results
781        if CSVRecordEvent.failed():  # FIXME(JA): make this implicit?
782            pytest.fail(CSVRecordEvent.result())

Perform a long charge / discharge.

def test_extended_cycle(self):
738    def test_extended_cycle(self):
739        """
740        | Description          | Perform a long charge / discharge                                      |
741        | :------------------- | :--------------------------------------------------------------------- |
742        | GitHub Issue         | turnaroundfactor/HITL#342                                       |
743        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
744jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
745        | MIL-PRF Sections     | 4.7.2.3 (Capacity discharge)                                      </br>\
746                                 4.6.1 (Standard Charge)                                           </br>\
747                                 4.3.1 (Normal conditions)                                         </br>\
748                                 3.5.3 (Capacity)                                                       |
749        | Instructions         | 1. Set thermistors to 23C                                         </br>\
750                                 2. Put cells in a rested state at 2.5V per cell                   </br>\
751                                 3. Charge battery (16.8V / 3A / 100 mA cutoff)                    </br>\
752                                 4. Rest for 3.1 hours (we want to calculate SoH change which requires 3 hours) </br>\
753                                 5. Discharge at 2A until battery reaches 10V then stop discharging     |
754        | Pass / Fail Criteria | ⦁ Serial cell voltage agrees with HITL cell voltage to within 1%  </br>\
755                                 ⦁ Serial terminal current agrees with HITL current to within 1%   </br>\
756                                 ⦁ Serial terminal voltage agrees with HITL voltage to within 1%   </br>\
757                                 ⦁ Serial thermistor 1 and 2 agree with HITL thermistor 1 and 2 to within 5°C </br>\
758                                 ⦁ No Fault Flags over entire duration of test                     </br>\
759                                 ⦁ No resets occur over entire duration of test                    </br>\
760                                 ⦁ Serial cell SOC agrees with lowest HITL cell SOC to within 5% SOC </br>\
761                                 ⦁ Serial Health shall only change once                            </br>\
762                                 ⦁ Serial Health shall not change by more than 1%                  </br>\
763                                 ⦁ Serial used Ah agrees with HITL Ah to within 1% (for abs(HITL Ah > 100mAh))  </br>\
764                                 ⦁ Serial charge cycle shall only increment once                   </br>\
765                                 ⦁ E-ink display is operational [TBD How to do this]                    |
766        | Estimated Duration   | 12 hours                                                               |
767        | Note                 | MIL-PRF 4.7.2.3.1 (Initial capacity discharge): Each battery subjected \
768                                 to the capacity discharge test above (see 4.7.2.3) on its initial      \
769                                 charge/discharge cycle is permitted up to three cycles to meet the     \
770                                 capacity discharge test requirement (see 3.1). Any battery not meeting \
771                                 the specified capacity discharge requirement (see 3.1) during any of   \
772                                 its first three cycles is considered a failure                         |
773        """
774        # FIXME(JA): adjust estimated duration based on first test
775
776        standard_charge()
777        standard_rest(seconds=30 if FAST_MODE else 3.1 * 3600)
778        standard_discharge()
779
780        # Check results
781        if CSVRecordEvent.failed():  # FIXME(JA): make this implicit?
782            pytest.fail(CSVRecordEvent.result())
Description Perform a long charge / discharge
GitHub Issue turnaroundfactor/HITL#342
Google Docs Google Sheet Cell
MIL-PRF Sections 4.7.2.3 (Capacity discharge)
4.6.1 (Standard Charge)
4.3.1 (Normal conditions)
3.5.3 (Capacity)
Instructions 1. Set thermistors to 23C
2. Put cells in a rested state at 2.5V per cell
3. Charge battery (16.8V / 3A / 100 mA cutoff)
4. Rest for 3.1 hours (we want to calculate SoH change which requires 3 hours)
5. Discharge at 2A until battery reaches 10V then stop discharging
Pass / Fail Criteria ⦁ Serial cell voltage agrees with HITL cell voltage to within 1%
⦁ Serial terminal current agrees with HITL current to within 1%
⦁ Serial terminal voltage agrees with HITL voltage to within 1%
⦁ Serial thermistor 1 and 2 agree with HITL thermistor 1 and 2 to within 5°C
⦁ No Fault Flags over entire duration of test
⦁ No resets occur over entire duration of test
⦁ Serial cell SOC agrees with lowest HITL cell SOC to within 5% SOC
⦁ Serial Health shall only change once
⦁ Serial Health shall not change by more than 1%
⦁ Serial used Ah agrees with HITL Ah to within 1% (for abs(HITL Ah > 100mAh))
⦁ Serial charge cycle shall only increment once
⦁ E-ink display is operational [TBD How to do this]
Estimated Duration 12 hours
Note MIL-PRF 4.7.2.3.1 (Initial capacity discharge): Each battery subjected to the capacity discharge test above (see 4.7.2.3) on its initial charge/discharge cycle is permitted up to three cycles to meet the capacity discharge test requirement (see 3.1). Any battery not meeting the specified capacity discharge requirement (see 3.1) during any of its first three cycles is considered a failure
pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 2.5}]), kwargs={'indirect': True})]
@pytest.mark.parametrize('reset_test_environment', [{'volts': 3.7}], indirect=True)
class TestSMBusWritableRegisters:
785@pytest.mark.parametrize("reset_test_environment", [{"volts": 3.7}], indirect=True)
786class TestSMBusWritableRegisters:
787    """SMBus Writable Registers"""
788
789    def test_smbus_writable_registers(self) -> None:
790        """
791        | Description          | Validate SMBus Writable Registers                                      |
792        | :------------------- | :--------------------------------------------------------------------- |
793        | GitHub Issue         | turnaroundfactor/HITL#397                                       |
794        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
795jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
796        | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
797        | Instructions         | 1. Set thermistors to 23C                                         </br>\
798                                 2. Put cells in a rested state at 3.7V per cell                   </br>\
799                                 3. Host to Battery Read Word (Register 1): 0x00                   </br>\
800                                 5. Host to Battery Read Word (Register 2): 0x0438                 </br>\
801                                 6. Host to Battery Write Word (Register 2): 0xBEEF                </br>\
802                                 7. Host to Battery Read Word (Register 2): 0xBEEF                 </br>\
803                                 8. Host to Battery Read Word (Register 3): 0x000A                 </br>\
804                                 9. Host to Battery Write Word (Register 3): 0xBEEF                </br>\
805                                 10. Host to Battery Read Word (Register 3): 0xBEEF                </br>\
806                                 11. Host to Battery Write Word (Register 3): 0x000A               </br>\
807                                 12. Host to Battery Read Word (Register 3): 0x000A                </br>\
808                                 13. Host to Battery Read Word (Register 4): 000xx00x0010 (where x is "don't care") |
809        | Pass / Fail Criteria | ⦁ ManufacturerAccess (Battery Register 1) returns 0x00            </br>\
810                                 ⦁ Remaining Capacity Alarm (Battery Register 2) returns 0x0438    </br>\
811                                 ⦁ Remaining Capacity Alarm (Battery Register 2) returns 0xBEEF    </br>\
812                                 ⦁ Remaining Time Alarm (Battery Register 3) returns 0x000A        </br>\
813                                 ⦁ Remaining Time Alarm (Battery Register 3) returns 0xBEEF        </br>\
814                                 ⦁ Remaining Time Alarm (Battery Register 3) returns 0x000A        </br>\
815                                 ⦁ Battery Mode (Battery Register 4) returns 000xx00x0010               |
816        | Estimated Duration   | 30 seconds                                                              |
817        | Note                 | When specified (see 3.1), batteries shall be compliant with System     \
818                                 Management Bus (SMBus) Specification Revision 1.1 and Smart Battery    \
819                                 Data (SBData) Specification, version 1.1, with the exception that      \
820                                 SBData safety signal hardware requirements therein shall be replaced   \
821                                 with a charge enable when a charge enable is specified (see 3.1 and    \
822                                 3.5.6). Certification is required. Batteries shall be compatible       \
823                                 with appropriate Level 2 and Level 3 chargers (see 6.4.7).             \
824                                 When tested as specified in 4.7.2.15.1, SMBus data output shall be     \
825                                 accurate within +0/-5% of the actual state of charge for the battery   \
826                                 under test throughout the discharge. Manufacturer and battery data     \
827                                 shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V     \
828                                 logic circuitry. Pull-up resistors will be provided by the charger.    \
829                                 SMBus circuitry shall respond to a SMBus query within 2 seconds.       \
830                                 Smart batteries may act as master or slave on the bus, but must        \
831                                 perform bus master timing arbitration according to the SMBus           \
832                                specification when acting as master.                                    |
833        """
834        time.sleep(30)
835
836        assert (serial_data := serial_monitor.read())
837        ten_percent_capacity = float(serial_data["milliamp_hour_capacity"]) // 10
838        default_value = 0x000A
839        easy_spot_value = 0xBEEF
840        smbus_registers = [
841            {"sm_register": SMBusReg.MANUFACTURING_ACCESS, "data": [0]},
842            {
843                "sm_register": SMBusReg.REMAINING_CAPACITY_ALARM,
844                "data": [ten_percent_capacity, easy_spot_value],
845            },
846            {
847                "sm_register": SMBusReg.REMAINING_TIME_ALARM,
848                "data": [default_value, easy_spot_value, default_value],
849            },
850        ]
851        failed_tests = []
852
853        # Manufacturing Access, Remaining Capacity Alarm, Remaining Time Alarm
854        for smbus_register in smbus_registers:
855            register = cast(SMBusReg, smbus_register["sm_register"])
856            index = 0
857            for elem in map(int, cast(list[int], smbus_register["data"])):
858                if index > 0:
859                    logger.write_info_to_report(f"Writing {hex(elem)} to {register.fname}")
860                    _smbus.write_register(register, elem)
861
862                read_sm_response = _smbus.read_register(register)
863
864                expected_value = elem.to_bytes(2, byteorder="little")
865                if read_sm_response[1] != expected_value:
866                    logger.write_result_to_html_report(
867                        f"{register.fname} did not have expected result of {hex(elem)}, "
868                        f"instead was: {read_sm_response[1]!r}"
869                    )
870                    logger.write_warning_to_report(
871                        f"{register.fname} did not have expected result of {hex(elem)}, "
872                        f"instead was: {read_sm_response[1]!r}"
873                    )
874                    failed_tests.append(register.fname)
875                elif index == 0:
876                    message = f"{register.fname} passed reading default value {hex(elem)}"
877                    logger.write_result_to_html_report(message)
878                    logger.write_info_to_report(message)
879                else:
880                    message = f"{register.fname} had value correctly changed to {hex(elem)}"
881                    logger.write_result_to_html_report(message)
882                    logger.write_info_to_report(message)
883
884                index += 1
885
886        # BatteryMode
887        # TODO: BatteryMode will be updated at later date
888
889        # mask = 0b111001101111
890        # pattern = 0b000000000010  # 000xx00x0010
891        battery_mode = SMBusReg.BATTERY_MODE
892        read_bm_response = _smbus.read_register(battery_mode)
893        # bm_bytes = int.from_bytes(read_bm_response[1], byteorder="little")
894        # logger.write_info_to_report(f"Bytes of {battery_mode.fname}: {bm_bytes}")
895
896        # masked_response = mask & bm_bytes
897        expected_value = 0x0000.to_bytes(2, byteorder="little")
898        if read_bm_response[1] == expected_value:
899            logger.write_info_to_report(f"{battery_mode.fname} passed response check")
900        else:
901            logger.write_warning_to_report(f"{battery_mode.fname} did not have expected result of: 0x0000")
902            failed_tests.append(battery_mode.fname)
903
904        # Overall results report
905        if failed_tests:
906            failed_registers = list(dict.fromkeys(failed_tests))
907            message = f"{len(failed_registers)} register(s) failed at least one test: {', '.join(failed_registers)}"
908            logger.write_result_to_html_report(f"<font color='#990000'> {message}</font>")
909            pytest.fail(message)
910        else:
911            logger.write_result_to_html_report("All tested registers passed writable test")

SMBus Writable Registers

def test_smbus_writable_registers(self) -> None:
789    def test_smbus_writable_registers(self) -> None:
790        """
791        | Description          | Validate SMBus Writable Registers                                      |
792        | :------------------- | :--------------------------------------------------------------------- |
793        | GitHub Issue         | turnaroundfactor/HITL#397                                       |
794        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
795jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
796        | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
797        | Instructions         | 1. Set thermistors to 23C                                         </br>\
798                                 2. Put cells in a rested state at 3.7V per cell                   </br>\
799                                 3. Host to Battery Read Word (Register 1): 0x00                   </br>\
800                                 5. Host to Battery Read Word (Register 2): 0x0438                 </br>\
801                                 6. Host to Battery Write Word (Register 2): 0xBEEF                </br>\
802                                 7. Host to Battery Read Word (Register 2): 0xBEEF                 </br>\
803                                 8. Host to Battery Read Word (Register 3): 0x000A                 </br>\
804                                 9. Host to Battery Write Word (Register 3): 0xBEEF                </br>\
805                                 10. Host to Battery Read Word (Register 3): 0xBEEF                </br>\
806                                 11. Host to Battery Write Word (Register 3): 0x000A               </br>\
807                                 12. Host to Battery Read Word (Register 3): 0x000A                </br>\
808                                 13. Host to Battery Read Word (Register 4): 000xx00x0010 (where x is "don't care") |
809        | Pass / Fail Criteria | ⦁ ManufacturerAccess (Battery Register 1) returns 0x00            </br>\
810                                 ⦁ Remaining Capacity Alarm (Battery Register 2) returns 0x0438    </br>\
811                                 ⦁ Remaining Capacity Alarm (Battery Register 2) returns 0xBEEF    </br>\
812                                 ⦁ Remaining Time Alarm (Battery Register 3) returns 0x000A        </br>\
813                                 ⦁ Remaining Time Alarm (Battery Register 3) returns 0xBEEF        </br>\
814                                 ⦁ Remaining Time Alarm (Battery Register 3) returns 0x000A        </br>\
815                                 ⦁ Battery Mode (Battery Register 4) returns 000xx00x0010               |
816        | Estimated Duration   | 30 seconds                                                              |
817        | Note                 | When specified (see 3.1), batteries shall be compliant with System     \
818                                 Management Bus (SMBus) Specification Revision 1.1 and Smart Battery    \
819                                 Data (SBData) Specification, version 1.1, with the exception that      \
820                                 SBData safety signal hardware requirements therein shall be replaced   \
821                                 with a charge enable when a charge enable is specified (see 3.1 and    \
822                                 3.5.6). Certification is required. Batteries shall be compatible       \
823                                 with appropriate Level 2 and Level 3 chargers (see 6.4.7).             \
824                                 When tested as specified in 4.7.2.15.1, SMBus data output shall be     \
825                                 accurate within +0/-5% of the actual state of charge for the battery   \
826                                 under test throughout the discharge. Manufacturer and battery data     \
827                                 shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V     \
828                                 logic circuitry. Pull-up resistors will be provided by the charger.    \
829                                 SMBus circuitry shall respond to a SMBus query within 2 seconds.       \
830                                 Smart batteries may act as master or slave on the bus, but must        \
831                                 perform bus master timing arbitration according to the SMBus           \
832                                specification when acting as master.                                    |
833        """
834        time.sleep(30)
835
836        assert (serial_data := serial_monitor.read())
837        ten_percent_capacity = float(serial_data["milliamp_hour_capacity"]) // 10
838        default_value = 0x000A
839        easy_spot_value = 0xBEEF
840        smbus_registers = [
841            {"sm_register": SMBusReg.MANUFACTURING_ACCESS, "data": [0]},
842            {
843                "sm_register": SMBusReg.REMAINING_CAPACITY_ALARM,
844                "data": [ten_percent_capacity, easy_spot_value],
845            },
846            {
847                "sm_register": SMBusReg.REMAINING_TIME_ALARM,
848                "data": [default_value, easy_spot_value, default_value],
849            },
850        ]
851        failed_tests = []
852
853        # Manufacturing Access, Remaining Capacity Alarm, Remaining Time Alarm
854        for smbus_register in smbus_registers:
855            register = cast(SMBusReg, smbus_register["sm_register"])
856            index = 0
857            for elem in map(int, cast(list[int], smbus_register["data"])):
858                if index > 0:
859                    logger.write_info_to_report(f"Writing {hex(elem)} to {register.fname}")
860                    _smbus.write_register(register, elem)
861
862                read_sm_response = _smbus.read_register(register)
863
864                expected_value = elem.to_bytes(2, byteorder="little")
865                if read_sm_response[1] != expected_value:
866                    logger.write_result_to_html_report(
867                        f"{register.fname} did not have expected result of {hex(elem)}, "
868                        f"instead was: {read_sm_response[1]!r}"
869                    )
870                    logger.write_warning_to_report(
871                        f"{register.fname} did not have expected result of {hex(elem)}, "
872                        f"instead was: {read_sm_response[1]!r}"
873                    )
874                    failed_tests.append(register.fname)
875                elif index == 0:
876                    message = f"{register.fname} passed reading default value {hex(elem)}"
877                    logger.write_result_to_html_report(message)
878                    logger.write_info_to_report(message)
879                else:
880                    message = f"{register.fname} had value correctly changed to {hex(elem)}"
881                    logger.write_result_to_html_report(message)
882                    logger.write_info_to_report(message)
883
884                index += 1
885
886        # BatteryMode
887        # TODO: BatteryMode will be updated at later date
888
889        # mask = 0b111001101111
890        # pattern = 0b000000000010  # 000xx00x0010
891        battery_mode = SMBusReg.BATTERY_MODE
892        read_bm_response = _smbus.read_register(battery_mode)
893        # bm_bytes = int.from_bytes(read_bm_response[1], byteorder="little")
894        # logger.write_info_to_report(f"Bytes of {battery_mode.fname}: {bm_bytes}")
895
896        # masked_response = mask & bm_bytes
897        expected_value = 0x0000.to_bytes(2, byteorder="little")
898        if read_bm_response[1] == expected_value:
899            logger.write_info_to_report(f"{battery_mode.fname} passed response check")
900        else:
901            logger.write_warning_to_report(f"{battery_mode.fname} did not have expected result of: 0x0000")
902            failed_tests.append(battery_mode.fname)
903
904        # Overall results report
905        if failed_tests:
906            failed_registers = list(dict.fromkeys(failed_tests))
907            message = f"{len(failed_registers)} register(s) failed at least one test: {', '.join(failed_registers)}"
908            logger.write_result_to_html_report(f"<font color='#990000'> {message}</font>")
909            pytest.fail(message)
910        else:
911            logger.write_result_to_html_report("All tested registers passed writable test")
Description Validate SMBus Writable Registers
GitHub Issue turnaroundfactor/HITL#397
Google Docs Google Sheet Cell
MIL-PRF Sections 3.5.9.1 (SMBus)
Instructions 1. Set thermistors to 23C
2. Put cells in a rested state at 3.7V per cell
3. Host to Battery Read Word (Register 1): 0x00
5. Host to Battery Read Word (Register 2): 0x0438
6. Host to Battery Write Word (Register 2): 0xBEEF
7. Host to Battery Read Word (Register 2): 0xBEEF
8. Host to Battery Read Word (Register 3): 0x000A
9. Host to Battery Write Word (Register 3): 0xBEEF
10. Host to Battery Read Word (Register 3): 0xBEEF
11. Host to Battery Write Word (Register 3): 0x000A
12. Host to Battery Read Word (Register 3): 0x000A
13. Host to Battery Read Word (Register 4): 000xx00x0010 (where x is "don't care")
Pass / Fail Criteria ⦁ ManufacturerAccess (Battery Register 1) returns 0x00
⦁ Remaining Capacity Alarm (Battery Register 2) returns 0x0438
⦁ Remaining Capacity Alarm (Battery Register 2) returns 0xBEEF
⦁ Remaining Time Alarm (Battery Register 3) returns 0x000A
⦁ Remaining Time Alarm (Battery Register 3) returns 0xBEEF
⦁ Remaining Time Alarm (Battery Register 3) returns 0x000A
⦁ Battery Mode (Battery Register 4) returns 000xx00x0010
Estimated Duration 30 seconds
Note When specified (see 3.1), batteries shall be compliant with System Management Bus (SMBus) Specification Revision 1.1 and Smart Battery Data (SBData) Specification, version 1.1, with the exception that SBData safety signal hardware requirements therein shall be replaced with a charge enable when a charge enable is specified (see 3.1 and 3.5.6). Certification is required. Batteries shall be compatible with appropriate Level 2 and Level 3 chargers (see 6.4.7). When tested as specified in 4.7.2.15.1, SMBus data output shall be accurate within +0/-5% of the actual state of charge for the battery under test throughout the discharge. Manufacturer and battery data shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V logic circuitry. Pull-up resistors will be provided by the charger. SMBus circuitry shall respond to a SMBus query within 2 seconds. Smart batteries may act as master or slave on the bus, but must perform bus master timing arbitration according to the SMBus specification when acting as master.
pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 3.7}]), kwargs={'indirect': True})]
def analyze_register_response( requirements: list[dict[str, hitl_tester.modules.bms.smbus_types.SMBusReg | float] | dict[str, hitl_tester.modules.bms.smbus_types.SMBusReg | int] | dict[str, hitl_tester.modules.bms.smbus_types.SMBusReg | bool]], failed_list: list[str], at_rate: int) -> list[str]:
914def analyze_register_response(
915    requirements: list[dict[str, SMBusReg | float] | dict[str, SMBusReg | int] | dict[str, SMBusReg | bool]],
916    failed_list: list[str],
917    at_rate: int,
918) -> list[str]:
919    """Reads SMBus register response and validates"""
920
921    full_amount = 0
922    remaining_amount = 0
923
924    for elem in requirements:
925        register: SMBusReg = cast(SMBusReg, elem["register"])
926        requirement = elem["requirement"]
927
928        read_response = _smbus.read_register(register)
929        if requirement is True:
930            if not read_response[0]:
931                message = f"Invalid response for {register.fname}. Expected non-zero value, but got {read_response[0]}"
932                logger.write_warning_to_report(message)
933                failed_list.append(f"{register.fname} after setting AtRate value to {at_rate}(mA)")
934            message = f"Received valid response for {register.fname}."
935            logger.write_info_to_report(message)
936
937        else:
938            assert isinstance(read_response[0], int | float) and isinstance(requirement, int | float)
939            if not math.isclose(read_response[0], requirement, rel_tol=0.1):
940                message = f"Invalid response for {register.fname}. Expected {requirement}, but got {read_response[0]}"
941                logger.write_warning_to_report(message)
942                failed_list.append(f"{register.fname} after setting AtRate value to {at_rate}(mA)")
943
944            message = f"Received valid response for {register.fname}"
945            logger.write_info_to_report(message)
946
947        if register == SMBusReg.AT_RATE_TIME_TO_FULL:
948            assert isinstance(read_response[0], int)
949            full_amount = read_response[0]
950
951        if register == SMBusReg.AT_RATE_TIME_TO_EMPTY:
952            assert isinstance(read_response[0], int)
953            remaining_amount = read_response[0]
954
955    logger.write_result_to_html_report(f"{at_rate} AtRate, {remaining_amount} Remaining, {full_amount} Full")
956    return failed_list

Reads SMBus register response and validates

class TestSMBusAtRate:
 960class TestSMBusAtRate:
 961    """Validate the SMBus AtRate Commands"""
 962
 963    def test_smbus_at_rate(self):
 964        # TODO: Update Documentation
 965        """
 966        | Description          | Validate SMBus AtRate Commands                                         |
 967        | :------------------- | :--------------------------------------------------------------------- |
 968        | GitHub Issue         | turnaroundfactor/HITL#398                                       |
 969        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
 970jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
 971        | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
 972        | Instructions         | 1. Write word 1000 (unit is mA) to AtRate[Register 0x4]           </br>\
 973                                 2. Read word from AtRateTimeToFull[0x5]                           </br>\
 974                                 3. Read word from AtRate TimeToEmpty[0x6]                         </br>\
 975                                 4. Read word from AtRateOK[0x7]                                   </br>\
 976                                 5. Write word -1000 to AtRate[Register 0x4]                       </br>\
 977                                 6. Read word from AtRateTimeToFull[0x5]                           </br>\
 978                                 7. Read word from AtRateTimeToEmpty[0x6]                          </br>\
 979                                 8. Charge battery at 2A                                           </br>\
 980                                 9. Read word from AtRateOK[0x7]                                   </br>\
 981                                 10. Charge battery at 0.1A                                        </br>\
 982                                 11. Read word from AtRateOK[0x7]                                  </br>\
 983                                 12. Discharge battery at 2A                                       </br>\
 984                                 13. Read word from AtRateOK[0x7]                                  </br>\
 985                                 14. Discharge battery at 0.1A                                     </br>\
 986                                 15. Read word from AtRateOK[0x7]                                  </br>\
 987                                 16. Write word 0 to AtRate[Register 0x4]                          </br>\
 988                                 17. Read word from AtRateTimeToFull[0x6]                          </br>\
 989                                 18. Read word from AtRateTimeToEmpty[0x6]                         </br>\
 990                                 19. Read word from AtRateOK[0x7]                                  </br>\
 991                                 20. Write word 0x8000 to BatteryMode[Register 0x3]                </br>\
 992                                 21. Write word 0x0808 to BatteryMode[Register 0x3]                     |
 993        | Pass / Fail Criteria | Charging (mA) ----                                                </br>\
 994                                 ⦁ Expect AtRateTimeToFull to be FullChargeCapacity-RemainingCapacity/1000/60 </br>\
 995                                 ⦁ Expect AtRateTimeToEmpty to be 65,635                           </br>\
 996                                 ⦁ Expect AtRateOK to be True (non-zero)                           </br>\
 997                                 Discharging(ma) ----                                              </br>\
 998                                 ⦁ Expect AtRateTimeToFull to be 65,535                            </br>\
 999                                 ⦁ Expect AtRateTimeToEmpty to be RemainingCapacity / 1000 / 60    </br>\
1000                                 ⦁ Expect AtRateOK to be True (non-zero) for all charge changes    </br>\
1001                                 Rest (mA) ----                                                    </br>\
1002                                 ⦁ Expect AtRAteTimeToFull to be 65,535                            </br>\
1003                                 ⦁ Expect AtRateTimeToEmpty to be 65,535                           </br>\
1004                                 ⦁ Expect AtRateOK to be True (non-zero)                                |
1005        | Estimated Duration   | 10 seconds                                                             |
1006        | Note                 | When specified (see 3.1), batteries shall be compliant with System     \
1007                                 Management Bus (SMBus) Specification Revision 1.1 and Smart Battery    \
1008                                 Data (SBData) Specification, version 1.1, with the exception that      \
1009                                 SBData safety signal hardware requirements therein shall be replaced   \
1010                                 with a charge enable when a charge enable is specified (see 3.1 and    \
1011                                 3.5.6). Certification is required. Batteries shall be compatible       \
1012                                 with appropriate Level 2 and Level 3 chargers (see 6.4.7).             \
1013                                 When tested as specified in 4.7.2.15.1, SMBus data output shall be     \
1014                                 accurate within +0/-5% of the actual state of charge for the battery   \
1015                                 under test throughout the discharge. Manufacturer and battery data     \
1016                                 shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V     \
1017                                 logic circuitry. Pull-up resistors will be provided by the charger.    \
1018                                 SMBus circuitry shall respond to a SMBus query within 2 seconds.       \
1019                                 Smart batteries may act as master or slave on the bus, but must        \
1020                                 perform bus master timing arbitration according to the SMBus           \
1021                                specification when acting as master.                                    |
1022        """
1023        time.sleep(30)
1024
1025        at_rate_register = SMBusReg.AT_RATE
1026        at_rate_time_to_full_register = SMBusReg.AT_RATE_TIME_TO_FULL
1027        at_rate_time_to_empty_register = SMBusReg.AT_RATE_TIME_TO_EMPTY
1028        at_rate_ok_register = SMBusReg.AT_RATE_OK
1029        #
1030        failed_tests = []
1031
1032        #
1033        # # Charging(mA)
1034        logger.write_info_to_report("Setting AtRate value to 1000(mA)")
1035        _smbus.write_register(at_rate_register, 1000)
1036        time.sleep(2)
1037        at_rate_response = _smbus.read_register(at_rate_register)
1038        logger.write_info_to_report(f"AtRate is now: {at_rate_response}")
1039
1040        full_charge = _smbus.read_register(SMBusReg.FULL_CHARGE_CAPACITY)
1041        remaining_capacity = _smbus.read_register(SMBusReg.REMAINING_CAPACITY)
1042        calculated_time_to_full = (full_charge[0] - remaining_capacity[0]) / 1000 * 60 / 0.975
1043
1044        charging_elems = [
1045            {"register": at_rate_time_to_full_register, "requirement": calculated_time_to_full},
1046            {"register": at_rate_time_to_empty_register, "requirement": 65535},
1047            {"register": at_rate_ok_register, "requirement": True},
1048        ]
1049
1050        failed_tests = analyze_register_response(charging_elems, failed_tests, at_rate_response[0])
1051
1052        # # Discharging
1053        logger.write_info_to_report("Setting AtRate value to -1000(mA)")
1054        _smbus.write_register(at_rate_register, -1000)
1055        time.sleep(2)
1056        at_rate_response = _smbus.read_register(at_rate_register)
1057        logger.write_info_to_report(f"AtRate is now: {at_rate_response}")
1058        remaining_capacity = _smbus.read_register(SMBusReg.REMAINING_CAPACITY)
1059        empty_requirement = remaining_capacity[0] / 1000 * 60 * 0.975
1060
1061        discharging_elems = [
1062            {"register": at_rate_time_to_full_register, "requirement": 65535},
1063            {"register": at_rate_time_to_empty_register, "requirement": empty_requirement},
1064        ]
1065
1066        failed_tests = analyze_register_response(discharging_elems, failed_tests, at_rate_response[0])
1067
1068        # AtRateOK -- Charging & Discharging
1069        rates = [
1070            {"charge": True, "rate": 2},
1071            {"charge": True, "rate": 0.1},
1072            {"charge": False, "rate": 2},
1073            {"charge": False, "rate": 0.1},
1074        ]
1075
1076        for elem in rates:
1077            if elem["charge"]:
1078                logger.write_info_to_report(f"Charging battery at {elem['rate']}A")
1079                with _bms.charger(16.8, elem["rate"]):
1080                    time.sleep(2)
1081                    at_rate_ok_response = _smbus.read_register(at_rate_ok_register)
1082                    if not at_rate_ok_response[0]:
1083                        message = (
1084                            f"Invalid response for {at_rate_ok_register.fname}. "
1085                            f"Expected non-zero value, but got {at_rate_ok_response[0]}"
1086                        )
1087                        logger.write_warning_to_report(message)
1088                        failed_tests.append(
1089                            f"{at_rate_ok_register.fname} after setting AtRate value to {at_rate_response[0]}(mA)"
1090                        )
1091                    else:
1092                        logger.write_result_to_html_report(
1093                            f"{at_rate_ok_register.fname} had expected value of True after charging at {elem['rate']}A"
1094                        )
1095            else:
1096                logger.write_info_to_report(f"Discharging battery at {elem['rate']}A")
1097                with _bms.load(elem["rate"]):
1098                    time.sleep(2)
1099                    at_rate_ok_response = _smbus.read_register(at_rate_ok_register)
1100                    if not at_rate_ok_response[0]:
1101                        message = (
1102                            f"Invalid response for {at_rate_ok_register.fname}. "
1103                            f"Expected non-zero value, but got {at_rate_ok_response[0]}"
1104                        )
1105                        logger.write_warning_to_report(message)
1106                        failed_tests.append(
1107                            f"{at_rate_ok_register.fname} after setting AtRate value to {at_rate_response[0]}(mA)"
1108                        )
1109                    else:
1110                        logger.write_result_to_html_report(
1111                            f"{at_rate_ok_register.fname} had expected value of True "
1112                            f"after discharging at {elem['rate']}A"
1113                        )
1114        # Rest(mA)
1115        logger.write_info_to_report("Setting AtRate value to 0(mA)")
1116        _smbus.write_register(at_rate_register, 0)
1117        time.sleep(2)
1118
1119        at_rate_response = _smbus.read_register(at_rate_register)
1120        logger.write_info_to_report(f"AtRate is now: {at_rate_response}")
1121        full_charge_three = _smbus.read_register(SMBusReg.FULL_CHARGE_CAPACITY)
1122        logger.write_info_to_report(f"{SMBusReg.FULL_CHARGE_CAPACITY.fname}: {full_charge_three}")
1123
1124        remaining_capacity_three = _smbus.read_register(SMBusReg.REMAINING_CAPACITY)
1125        logger.write_info_to_report(f"{SMBusReg.REMAINING_CAPACITY.fname}: {remaining_capacity_three}")
1126        resting_elems = [
1127            {"register": at_rate_time_to_full_register, "requirement": 65535},
1128            {"register": at_rate_time_to_empty_register, "requirement": 65535},
1129            {"register": at_rate_ok_register, "requirement": True},
1130        ]
1131
1132        failed_tests = analyze_register_response(resting_elems, failed_tests, at_rate_response[0])
1133
1134        # Power Mode
1135        battery_mode_register = SMBusReg.BATTERY_MODE
1136        writing_value = 0x8000
1137        print(f"Writing {writing_value} to {battery_mode_register.fname}")
1138        _smbus.write_register(battery_mode_register, writing_value)
1139        batt_response = _smbus.read_register(battery_mode_register)
1140        logger.write_info_to_report(f"{battery_mode_register.fname} after writing {writing_value}: {batt_response}")
1141        batt_bytes = batt_response[1]
1142        batt_analyzed = BatteryMode(batt_bytes)
1143        logger.write_info_to_report(f"Capacity Mode after writing {writing_value}: {batt_analyzed.capacity_mode}")
1144
1145        writing_value = 0x8080
1146        print(f"Writing {writing_value} to {battery_mode_register.fname}")
1147        _smbus.write_register(battery_mode_register, writing_value)
1148
1149        batt_mode_response = _smbus.read_register(battery_mode_register)
1150        logger.write_info_to_report(
1151            f"{battery_mode_register.fname} after writing {writing_value}: {batt_mode_response}"
1152        )
1153        batt_bytes = batt_mode_response[1]
1154        batt_analyzed = BatteryMode(batt_bytes)
1155        logger.write_info_to_report(f"Capacity Mode after writing {writing_value}: {batt_analyzed.capacity_mode}")
1156        if batt_analyzed.capacity_mode is not True:
1157            logger.write_warning_to_report(
1158                f"{battery_mode_register.fname} did not have Capacity Mode with expected "
1159                f"value of True, instead received {batt_analyzed.capacity_mode}"
1160            )
1161            failed_tests.append(f"{battery_mode_register.fname} did not have expected Capacity Mode value")
1162        else:
1163            logger.write_result_to_html_report(
1164                f"{battery_mode_register.fname} had Capacity Mode with expected value of {batt_analyzed.capacity_mode}"
1165            )
1166
1167        if failed_tests:
1168            message = f"{len(failed_tests)} AtRate tests failed: {', '.join(failed_tests)}"
1169            logger.write_warning_to_report(message)
1170            pytest.fail(f"<font color='#990000'>{message}</font>")
1171
1172        logger.write_result_to_html_report("All AtRate SMBus tests passed")

Validate the SMBus AtRate Commands

def test_smbus_at_rate(self):
 963    def test_smbus_at_rate(self):
 964        # TODO: Update Documentation
 965        """
 966        | Description          | Validate SMBus AtRate Commands                                         |
 967        | :------------------- | :--------------------------------------------------------------------- |
 968        | GitHub Issue         | turnaroundfactor/HITL#398                                       |
 969        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
 970jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
 971        | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
 972        | Instructions         | 1. Write word 1000 (unit is mA) to AtRate[Register 0x4]           </br>\
 973                                 2. Read word from AtRateTimeToFull[0x5]                           </br>\
 974                                 3. Read word from AtRate TimeToEmpty[0x6]                         </br>\
 975                                 4. Read word from AtRateOK[0x7]                                   </br>\
 976                                 5. Write word -1000 to AtRate[Register 0x4]                       </br>\
 977                                 6. Read word from AtRateTimeToFull[0x5]                           </br>\
 978                                 7. Read word from AtRateTimeToEmpty[0x6]                          </br>\
 979                                 8. Charge battery at 2A                                           </br>\
 980                                 9. Read word from AtRateOK[0x7]                                   </br>\
 981                                 10. Charge battery at 0.1A                                        </br>\
 982                                 11. Read word from AtRateOK[0x7]                                  </br>\
 983                                 12. Discharge battery at 2A                                       </br>\
 984                                 13. Read word from AtRateOK[0x7]                                  </br>\
 985                                 14. Discharge battery at 0.1A                                     </br>\
 986                                 15. Read word from AtRateOK[0x7]                                  </br>\
 987                                 16. Write word 0 to AtRate[Register 0x4]                          </br>\
 988                                 17. Read word from AtRateTimeToFull[0x6]                          </br>\
 989                                 18. Read word from AtRateTimeToEmpty[0x6]                         </br>\
 990                                 19. Read word from AtRateOK[0x7]                                  </br>\
 991                                 20. Write word 0x8000 to BatteryMode[Register 0x3]                </br>\
 992                                 21. Write word 0x0808 to BatteryMode[Register 0x3]                     |
 993        | Pass / Fail Criteria | Charging (mA) ----                                                </br>\
 994                                 ⦁ Expect AtRateTimeToFull to be FullChargeCapacity-RemainingCapacity/1000/60 </br>\
 995                                 ⦁ Expect AtRateTimeToEmpty to be 65,635                           </br>\
 996                                 ⦁ Expect AtRateOK to be True (non-zero)                           </br>\
 997                                 Discharging(ma) ----                                              </br>\
 998                                 ⦁ Expect AtRateTimeToFull to be 65,535                            </br>\
 999                                 ⦁ Expect AtRateTimeToEmpty to be RemainingCapacity / 1000 / 60    </br>\
1000                                 ⦁ Expect AtRateOK to be True (non-zero) for all charge changes    </br>\
1001                                 Rest (mA) ----                                                    </br>\
1002                                 ⦁ Expect AtRAteTimeToFull to be 65,535                            </br>\
1003                                 ⦁ Expect AtRateTimeToEmpty to be 65,535                           </br>\
1004                                 ⦁ Expect AtRateOK to be True (non-zero)                                |
1005        | Estimated Duration   | 10 seconds                                                             |
1006        | Note                 | When specified (see 3.1), batteries shall be compliant with System     \
1007                                 Management Bus (SMBus) Specification Revision 1.1 and Smart Battery    \
1008                                 Data (SBData) Specification, version 1.1, with the exception that      \
1009                                 SBData safety signal hardware requirements therein shall be replaced   \
1010                                 with a charge enable when a charge enable is specified (see 3.1 and    \
1011                                 3.5.6). Certification is required. Batteries shall be compatible       \
1012                                 with appropriate Level 2 and Level 3 chargers (see 6.4.7).             \
1013                                 When tested as specified in 4.7.2.15.1, SMBus data output shall be     \
1014                                 accurate within +0/-5% of the actual state of charge for the battery   \
1015                                 under test throughout the discharge. Manufacturer and battery data     \
1016                                 shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V     \
1017                                 logic circuitry. Pull-up resistors will be provided by the charger.    \
1018                                 SMBus circuitry shall respond to a SMBus query within 2 seconds.       \
1019                                 Smart batteries may act as master or slave on the bus, but must        \
1020                                 perform bus master timing arbitration according to the SMBus           \
1021                                specification when acting as master.                                    |
1022        """
1023        time.sleep(30)
1024
1025        at_rate_register = SMBusReg.AT_RATE
1026        at_rate_time_to_full_register = SMBusReg.AT_RATE_TIME_TO_FULL
1027        at_rate_time_to_empty_register = SMBusReg.AT_RATE_TIME_TO_EMPTY
1028        at_rate_ok_register = SMBusReg.AT_RATE_OK
1029        #
1030        failed_tests = []
1031
1032        #
1033        # # Charging(mA)
1034        logger.write_info_to_report("Setting AtRate value to 1000(mA)")
1035        _smbus.write_register(at_rate_register, 1000)
1036        time.sleep(2)
1037        at_rate_response = _smbus.read_register(at_rate_register)
1038        logger.write_info_to_report(f"AtRate is now: {at_rate_response}")
1039
1040        full_charge = _smbus.read_register(SMBusReg.FULL_CHARGE_CAPACITY)
1041        remaining_capacity = _smbus.read_register(SMBusReg.REMAINING_CAPACITY)
1042        calculated_time_to_full = (full_charge[0] - remaining_capacity[0]) / 1000 * 60 / 0.975
1043
1044        charging_elems = [
1045            {"register": at_rate_time_to_full_register, "requirement": calculated_time_to_full},
1046            {"register": at_rate_time_to_empty_register, "requirement": 65535},
1047            {"register": at_rate_ok_register, "requirement": True},
1048        ]
1049
1050        failed_tests = analyze_register_response(charging_elems, failed_tests, at_rate_response[0])
1051
1052        # # Discharging
1053        logger.write_info_to_report("Setting AtRate value to -1000(mA)")
1054        _smbus.write_register(at_rate_register, -1000)
1055        time.sleep(2)
1056        at_rate_response = _smbus.read_register(at_rate_register)
1057        logger.write_info_to_report(f"AtRate is now: {at_rate_response}")
1058        remaining_capacity = _smbus.read_register(SMBusReg.REMAINING_CAPACITY)
1059        empty_requirement = remaining_capacity[0] / 1000 * 60 * 0.975
1060
1061        discharging_elems = [
1062            {"register": at_rate_time_to_full_register, "requirement": 65535},
1063            {"register": at_rate_time_to_empty_register, "requirement": empty_requirement},
1064        ]
1065
1066        failed_tests = analyze_register_response(discharging_elems, failed_tests, at_rate_response[0])
1067
1068        # AtRateOK -- Charging & Discharging
1069        rates = [
1070            {"charge": True, "rate": 2},
1071            {"charge": True, "rate": 0.1},
1072            {"charge": False, "rate": 2},
1073            {"charge": False, "rate": 0.1},
1074        ]
1075
1076        for elem in rates:
1077            if elem["charge"]:
1078                logger.write_info_to_report(f"Charging battery at {elem['rate']}A")
1079                with _bms.charger(16.8, elem["rate"]):
1080                    time.sleep(2)
1081                    at_rate_ok_response = _smbus.read_register(at_rate_ok_register)
1082                    if not at_rate_ok_response[0]:
1083                        message = (
1084                            f"Invalid response for {at_rate_ok_register.fname}. "
1085                            f"Expected non-zero value, but got {at_rate_ok_response[0]}"
1086                        )
1087                        logger.write_warning_to_report(message)
1088                        failed_tests.append(
1089                            f"{at_rate_ok_register.fname} after setting AtRate value to {at_rate_response[0]}(mA)"
1090                        )
1091                    else:
1092                        logger.write_result_to_html_report(
1093                            f"{at_rate_ok_register.fname} had expected value of True after charging at {elem['rate']}A"
1094                        )
1095            else:
1096                logger.write_info_to_report(f"Discharging battery at {elem['rate']}A")
1097                with _bms.load(elem["rate"]):
1098                    time.sleep(2)
1099                    at_rate_ok_response = _smbus.read_register(at_rate_ok_register)
1100                    if not at_rate_ok_response[0]:
1101                        message = (
1102                            f"Invalid response for {at_rate_ok_register.fname}. "
1103                            f"Expected non-zero value, but got {at_rate_ok_response[0]}"
1104                        )
1105                        logger.write_warning_to_report(message)
1106                        failed_tests.append(
1107                            f"{at_rate_ok_register.fname} after setting AtRate value to {at_rate_response[0]}(mA)"
1108                        )
1109                    else:
1110                        logger.write_result_to_html_report(
1111                            f"{at_rate_ok_register.fname} had expected value of True "
1112                            f"after discharging at {elem['rate']}A"
1113                        )
1114        # Rest(mA)
1115        logger.write_info_to_report("Setting AtRate value to 0(mA)")
1116        _smbus.write_register(at_rate_register, 0)
1117        time.sleep(2)
1118
1119        at_rate_response = _smbus.read_register(at_rate_register)
1120        logger.write_info_to_report(f"AtRate is now: {at_rate_response}")
1121        full_charge_three = _smbus.read_register(SMBusReg.FULL_CHARGE_CAPACITY)
1122        logger.write_info_to_report(f"{SMBusReg.FULL_CHARGE_CAPACITY.fname}: {full_charge_three}")
1123
1124        remaining_capacity_three = _smbus.read_register(SMBusReg.REMAINING_CAPACITY)
1125        logger.write_info_to_report(f"{SMBusReg.REMAINING_CAPACITY.fname}: {remaining_capacity_three}")
1126        resting_elems = [
1127            {"register": at_rate_time_to_full_register, "requirement": 65535},
1128            {"register": at_rate_time_to_empty_register, "requirement": 65535},
1129            {"register": at_rate_ok_register, "requirement": True},
1130        ]
1131
1132        failed_tests = analyze_register_response(resting_elems, failed_tests, at_rate_response[0])
1133
1134        # Power Mode
1135        battery_mode_register = SMBusReg.BATTERY_MODE
1136        writing_value = 0x8000
1137        print(f"Writing {writing_value} to {battery_mode_register.fname}")
1138        _smbus.write_register(battery_mode_register, writing_value)
1139        batt_response = _smbus.read_register(battery_mode_register)
1140        logger.write_info_to_report(f"{battery_mode_register.fname} after writing {writing_value}: {batt_response}")
1141        batt_bytes = batt_response[1]
1142        batt_analyzed = BatteryMode(batt_bytes)
1143        logger.write_info_to_report(f"Capacity Mode after writing {writing_value}: {batt_analyzed.capacity_mode}")
1144
1145        writing_value = 0x8080
1146        print(f"Writing {writing_value} to {battery_mode_register.fname}")
1147        _smbus.write_register(battery_mode_register, writing_value)
1148
1149        batt_mode_response = _smbus.read_register(battery_mode_register)
1150        logger.write_info_to_report(
1151            f"{battery_mode_register.fname} after writing {writing_value}: {batt_mode_response}"
1152        )
1153        batt_bytes = batt_mode_response[1]
1154        batt_analyzed = BatteryMode(batt_bytes)
1155        logger.write_info_to_report(f"Capacity Mode after writing {writing_value}: {batt_analyzed.capacity_mode}")
1156        if batt_analyzed.capacity_mode is not True:
1157            logger.write_warning_to_report(
1158                f"{battery_mode_register.fname} did not have Capacity Mode with expected "
1159                f"value of True, instead received {batt_analyzed.capacity_mode}"
1160            )
1161            failed_tests.append(f"{battery_mode_register.fname} did not have expected Capacity Mode value")
1162        else:
1163            logger.write_result_to_html_report(
1164                f"{battery_mode_register.fname} had Capacity Mode with expected value of {batt_analyzed.capacity_mode}"
1165            )
1166
1167        if failed_tests:
1168            message = f"{len(failed_tests)} AtRate tests failed: {', '.join(failed_tests)}"
1169            logger.write_warning_to_report(message)
1170            pytest.fail(f"<font color='#990000'>{message}</font>")
1171
1172        logger.write_result_to_html_report("All AtRate SMBus tests passed")
Description Validate SMBus AtRate Commands
GitHub Issue turnaroundfactor/HITL#398
Google Docs Google Sheet Cell
MIL-PRF Sections 3.5.9.1 (SMBus)
Instructions 1. Write word 1000 (unit is mA) to AtRate[Register 0x4]
2. Read word from AtRateTimeToFull[0x5]
3. Read word from AtRate TimeToEmpty[0x6]
4. Read word from AtRateOK[0x7]
5. Write word -1000 to AtRate[Register 0x4]
6. Read word from AtRateTimeToFull[0x5]
7. Read word from AtRateTimeToEmpty[0x6]
8. Charge battery at 2A
9. Read word from AtRateOK[0x7]
10. Charge battery at 0.1A
11. Read word from AtRateOK[0x7]
12. Discharge battery at 2A
13. Read word from AtRateOK[0x7]
14. Discharge battery at 0.1A
15. Read word from AtRateOK[0x7]
16. Write word 0 to AtRate[Register 0x4]
17. Read word from AtRateTimeToFull[0x6]
18. Read word from AtRateTimeToEmpty[0x6]
19. Read word from AtRateOK[0x7]
20. Write word 0x8000 to BatteryMode[Register 0x3]
21. Write word 0x0808 to BatteryMode[Register 0x3]
Pass / Fail Criteria Charging (mA) ----
⦁ Expect AtRateTimeToFull to be FullChargeCapacity-RemainingCapacity/1000/60
⦁ Expect AtRateTimeToEmpty to be 65,635
⦁ Expect AtRateOK to be True (non-zero)
Discharging(ma) ----
⦁ Expect AtRateTimeToFull to be 65,535
⦁ Expect AtRateTimeToEmpty to be RemainingCapacity / 1000 / 60
⦁ Expect AtRateOK to be True (non-zero) for all charge changes
Rest (mA) ----
⦁ Expect AtRAteTimeToFull to be 65,535
⦁ Expect AtRateTimeToEmpty to be 65,535
⦁ Expect AtRateOK to be True (non-zero)
Estimated Duration 10 seconds
Note When specified (see 3.1), batteries shall be compliant with System Management Bus (SMBus) Specification Revision 1.1 and Smart Battery Data (SBData) Specification, version 1.1, with the exception that SBData safety signal hardware requirements therein shall be replaced with a charge enable when a charge enable is specified (see 3.1 and 3.5.6). Certification is required. Batteries shall be compatible with appropriate Level 2 and Level 3 chargers (see 6.4.7). When tested as specified in 4.7.2.15.1, SMBus data output shall be accurate within +0/-5% of the actual state of charge for the battery under test throughout the discharge. Manufacturer and battery data shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V logic circuitry. Pull-up resistors will be provided by the charger. SMBus circuitry shall respond to a SMBus query within 2 seconds. Smart batteries may act as master or slave on the bus, but must perform bus master timing arbitration according to the SMBus specification when acting as master.
@pytest.mark.parametrize('reset_test_environment', [{'soc': 0.5}], indirect=True)
class TestConstantSMBusValues:
1175@pytest.mark.parametrize("reset_test_environment", [{"soc": 0.50}], indirect=True)
1176class TestConstantSMBusValues:
1177    """Test constant SMBus values"""
1178
1179    def test_constant_smbus_values(self):
1180        """
1181           | Description          | Constant SMBus Values                                                  |
1182           | :------------------- | :--------------------------------------------------------------------- |
1183           | GitHub Issue         | turnaroundfactor/HITL#385                                       |
1184           | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1185   jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D12) |
1186           | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
1187           | Instructions         | 1. Read Device Chemistry [0x22]                                   </br>\
1188                                    2. Read Device Name [0x21]                                        </br>\
1189                                    3. Read Manufacturer Name [0x20]                                  </br>\
1190                                    4. Read Serial Number [0x1C]                                      </br>\
1191                                    5. Read Manufacturer Date [0x1B]                                  </br>\
1192                                    6. Read Specification Info [0x1A]                                 </br>\
1193                                    7. Read Design Voltage [0x19]                                     </br>\
1194                                    8. Read Design Capacity [0x18]                                    </br>\
1195                                    9. Read Charging Voltage [0x15]                                   </br>\
1196                                    10. Read Charging Current [0x14]                                  </br>\
1197                                    11. Read Manufacturer Access [0x00]                               </br>\
1198                                    12. Read Remaining Time Alarm [0x02]                              </br>\
1199                                    13. Read Remaining Capacity Alarm [0x01]                          </br>\
1200                                    14. Read AtRateOk [0x07]                                          </br>\
1201                                    15. Read AtRateTimeToEmpty [0x06]                                 </br>\
1202                                    16. Read AtRateTimeToFull [0x05]                                  </br>\
1203                                    17. Read AtRate [0x04]                                            </br>\
1204                                    18. Read Battery Mode [0x03]                                      </br>\
1205                                    19. Read Max Error [0x0C]                                         </br>\
1206                                    20. Read Full Charge Capacity [0x10]                                   |
1207           | Pass / Fail Criteria | Charging (mA) ----                                                </br>\
1208                                    ⦁ Expect Device Chemistry [0x22] to be LION                       </br>\
1209                                    ⦁ Expect Device Name [0x21] to be BB-2590/U                       </br>\
1210                                    ⦁ Expect Manufacturer Name [0x20] to be TURN-AROUND FACTOR        </br>\
1211                                    ⦁ Expect Serial Number [0x1C] to be a unique value                </br>\
1212                                    ⦁ Expect Manufacturer Date [0x1B] to be 0x0100                    </br>\
1213                                    ⦁ Expect Specification Info [0x1A] to be 0x0100                   </br>\
1214                                    ⦁ Expect Design Voltage [0x19] to be 16800                        </br>\
1215                                    ⦁ Expect Design Capacity [0x18] to be CAPACITY * 0.975            </br>\
1216                                    ⦁ Expect Charging Voltage [0x15] to be 16800                      </br>\
1217                                    ⦁ Expect Charging Current [0x14] to be 2000                       </br>\
1218                                    ⦁ Expect Manufacturer Access [0x00] to be 0                       </br>\
1219                                    ⦁ Expect Remaining Time Alarm [0x02] to be 10                     </br>\
1220                                    ⦁ Expect Remaining Capacity Alarm [0x01] to be SOC * Design Capacity  </br>\
1221                                    ⦁ Expect AtRateOk [0x07] to be True                               </br>\
1222                                    ⦁ Expect AtRateTimeToEmpty [0x06] to be 65535                     </br>\
1223                                    ⦁ Expect AtRateTimeToFull [0x05] to be 65535                      </br>\
1224                                    ⦁ Expect AtRate [0x04] to be 0                                    </br>\
1225                                    ⦁ Expect Battery Mode [0x03] to be 0                              </br>\
1226                                    ⦁ Expect Max Error [0x0C] to be 0                                 </br>\
1227                                    ⦁ Expect Full Charge Capacity [0x10] to be CAPACITY                    |
1228           | Estimated Duration   | 10 seconds                                                             |
1229           | Note                 | When specified (see 3.1), batteries shall be compliant with System     \
1230                                    Management Bus (SMBus) Specification Revision 1.1 and Smart Battery    \
1231                                    Data (SBData) Specification, version 1.1, with the exception that      \
1232                                    SBData safety signal hardware requirements therein shall be replaced   \
1233                                    with a charge enable when a charge enable is specified (see 3.1 and    \
1234                                    3.5.6). Certification is required. Batteries shall be compatible       \
1235                                    with appropriate Level 2 and Level 3 chargers (see 6.4.7).             \
1236                                    When tested as specified in 4.7.2.15.1, SMBus data output shall be     \
1237                                    accurate within +0/-5% of the actual state of charge for the battery   \
1238                                    under test throughout the discharge. Manufacturer and battery data     \
1239                                    shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V     \
1240                                    logic circuitry. Pull-up resistors will be provided by the charger.    \
1241                                    SMBus circuitry shall respond to a SMBus query within 2 seconds.       \
1242                                    Smart batteries may act as master or slave on the bus, but must        \
1243                                    perform bus master timing arbitration according to the SMBus           \
1244                                   specification when acting as master.                                    |
1245        """
1246        constant_elements = [
1247            {"register": SMBusReg.DEVICE_CHEMISTRY, "requirement": "LION"},
1248            {"register": SMBusReg.DEVICE_NAME, "requirement": "BB-2590/U"},
1249            {"register": SMBusReg.MANUFACTURER_NAME, "requirement": "TURN-AROUND FACTOR"},
1250            {"register": SMBusReg.SERIAL_NUM, "requirement": 0xFFFF},
1251            {"register": SMBusReg.MANUFACTURER_DATE, "requirement": None},
1252            {"register": SMBusReg.SPECIFICATION_INFO, "requirement": None},
1253            {"register": SMBusReg.DESIGN_VOLTAGE, "requirement": 16800},
1254            {
1255                "register": SMBusReg.DESIGN_CAPACITY,
1256                "requirement": math.floor(0.975 * CELL_CAPACITY_AH * 1000),
1257            },
1258            {"register": SMBusReg.CHARGING_VOLTAGE, "requirement": 16800},
1259            {"register": SMBusReg.CHARGING_CURRENT, "requirement": 2000},
1260            {"register": SMBusReg.MANUFACTURING_ACCESS, "requirement": 0},
1261            {"register": SMBusReg.REMAINING_TIME_ALARM, "requirement": 10},
1262            {"register": SMBusReg.REMAINING_CAPACITY, "requirement": 48},
1263            {"register": SMBusReg.AT_RATE_OK, "requirement": True},
1264            {"register": SMBusReg.AT_RATE_TIME_TO_EMPTY, "requirement": 65535},
1265            {"register": SMBusReg.AT_RATE_TIME_TO_FULL, "requirement": 65535},
1266            {"register": SMBusReg.AT_RATE, "requirement": 0},
1267            {"register": SMBusReg.BATTERY_MODE, "requirement": 0},
1268            {"register": SMBusReg.MAX_ERROR, "requirement": 0},
1269            {
1270                "register": SMBusReg.FULL_CHARGE_CAPACITY,
1271                "requirement": CELL_CAPACITY_AH * 1000,
1272            },
1273        ]
1274        test_failed = False
1275
1276        for element in constant_elements:
1277            register = element["register"]
1278            requirement = element["requirement"]
1279
1280            read_response = _smbus.read_register(register)
1281
1282            if register == SMBusReg.SERIAL_NUM:
1283                if read_response[0] != requirement:
1284                    logger.write_result_to_html_report(
1285                        f"{register.fname} had unique value of {read_response[0]} ({read_response[1]})"
1286                    )
1287                else:
1288                    logger.write_result_to_html_report(
1289                        f'<font color="#990000">{register.fname} was {read_response[0]} ({read_response[1]}) '
1290                        f"not a unique value</font>"
1291                    )
1292                    test_failed = True
1293                continue
1294
1295            if register == SMBusReg.REMAINING_CAPACITY:
1296                design_capacity = _smbus.read_register(SMBusReg.DESIGN_CAPACITY)
1297                requirement = design_capacity[0] // 2
1298                if requirement - 3 <= read_response[0] <= requirement + 3:
1299                    logger.write_result_to_html_report(f"{register.fname} had expected value of {requirement}")
1300                else:
1301                    logger.write_result_to_html_report(
1302                        f'<font color="#990000">{register.fname} was {read_response[0]} ({read_response[1]}) '
1303                        f"not {requirement}</font>"
1304                    )
1305                    test_failed = True
1306
1307            elif isinstance(requirement, int):
1308                if read_response[0] == requirement or read_response[1] == requirement.to_bytes(2, byteorder="little"):
1309                    logger.write_result_to_html_report(f"{register.fname} had expected value of {requirement}")
1310                else:
1311                    logger.write_result_to_html_report(
1312                        f'<font color="#990000">{register.fname} was {read_response[0]} ({read_response[1]}) '
1313                        f"not {requirement}</font>"
1314                    )
1315                    test_failed = True
1316            elif read_response[0] == requirement:
1317                logger.write_result_to_html_report(f"{register.fname} had expected value of {requirement}")
1318            else:
1319                logger.write_result_to_html_report(
1320                    f'<font color="#990000">{register.fname} was {read_response[0]} ({read_response[1]}) '
1321                    f"not {requirement}</font>"
1322                )
1323                test_failed = True
1324
1325        if test_failed:
1326            pytest.fail()
1327
1328        logger.write_result_to_html_report("All SMBus Constant Values passed")

Test constant SMBus values

def test_constant_smbus_values(self):
1179    def test_constant_smbus_values(self):
1180        """
1181           | Description          | Constant SMBus Values                                                  |
1182           | :------------------- | :--------------------------------------------------------------------- |
1183           | GitHub Issue         | turnaroundfactor/HITL#385                                       |
1184           | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1185   jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D12) |
1186           | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
1187           | Instructions         | 1. Read Device Chemistry [0x22]                                   </br>\
1188                                    2. Read Device Name [0x21]                                        </br>\
1189                                    3. Read Manufacturer Name [0x20]                                  </br>\
1190                                    4. Read Serial Number [0x1C]                                      </br>\
1191                                    5. Read Manufacturer Date [0x1B]                                  </br>\
1192                                    6. Read Specification Info [0x1A]                                 </br>\
1193                                    7. Read Design Voltage [0x19]                                     </br>\
1194                                    8. Read Design Capacity [0x18]                                    </br>\
1195                                    9. Read Charging Voltage [0x15]                                   </br>\
1196                                    10. Read Charging Current [0x14]                                  </br>\
1197                                    11. Read Manufacturer Access [0x00]                               </br>\
1198                                    12. Read Remaining Time Alarm [0x02]                              </br>\
1199                                    13. Read Remaining Capacity Alarm [0x01]                          </br>\
1200                                    14. Read AtRateOk [0x07]                                          </br>\
1201                                    15. Read AtRateTimeToEmpty [0x06]                                 </br>\
1202                                    16. Read AtRateTimeToFull [0x05]                                  </br>\
1203                                    17. Read AtRate [0x04]                                            </br>\
1204                                    18. Read Battery Mode [0x03]                                      </br>\
1205                                    19. Read Max Error [0x0C]                                         </br>\
1206                                    20. Read Full Charge Capacity [0x10]                                   |
1207           | Pass / Fail Criteria | Charging (mA) ----                                                </br>\
1208                                    ⦁ Expect Device Chemistry [0x22] to be LION                       </br>\
1209                                    ⦁ Expect Device Name [0x21] to be BB-2590/U                       </br>\
1210                                    ⦁ Expect Manufacturer Name [0x20] to be TURN-AROUND FACTOR        </br>\
1211                                    ⦁ Expect Serial Number [0x1C] to be a unique value                </br>\
1212                                    ⦁ Expect Manufacturer Date [0x1B] to be 0x0100                    </br>\
1213                                    ⦁ Expect Specification Info [0x1A] to be 0x0100                   </br>\
1214                                    ⦁ Expect Design Voltage [0x19] to be 16800                        </br>\
1215                                    ⦁ Expect Design Capacity [0x18] to be CAPACITY * 0.975            </br>\
1216                                    ⦁ Expect Charging Voltage [0x15] to be 16800                      </br>\
1217                                    ⦁ Expect Charging Current [0x14] to be 2000                       </br>\
1218                                    ⦁ Expect Manufacturer Access [0x00] to be 0                       </br>\
1219                                    ⦁ Expect Remaining Time Alarm [0x02] to be 10                     </br>\
1220                                    ⦁ Expect Remaining Capacity Alarm [0x01] to be SOC * Design Capacity  </br>\
1221                                    ⦁ Expect AtRateOk [0x07] to be True                               </br>\
1222                                    ⦁ Expect AtRateTimeToEmpty [0x06] to be 65535                     </br>\
1223                                    ⦁ Expect AtRateTimeToFull [0x05] to be 65535                      </br>\
1224                                    ⦁ Expect AtRate [0x04] to be 0                                    </br>\
1225                                    ⦁ Expect Battery Mode [0x03] to be 0                              </br>\
1226                                    ⦁ Expect Max Error [0x0C] to be 0                                 </br>\
1227                                    ⦁ Expect Full Charge Capacity [0x10] to be CAPACITY                    |
1228           | Estimated Duration   | 10 seconds                                                             |
1229           | Note                 | When specified (see 3.1), batteries shall be compliant with System     \
1230                                    Management Bus (SMBus) Specification Revision 1.1 and Smart Battery    \
1231                                    Data (SBData) Specification, version 1.1, with the exception that      \
1232                                    SBData safety signal hardware requirements therein shall be replaced   \
1233                                    with a charge enable when a charge enable is specified (see 3.1 and    \
1234                                    3.5.6). Certification is required. Batteries shall be compatible       \
1235                                    with appropriate Level 2 and Level 3 chargers (see 6.4.7).             \
1236                                    When tested as specified in 4.7.2.15.1, SMBus data output shall be     \
1237                                    accurate within +0/-5% of the actual state of charge for the battery   \
1238                                    under test throughout the discharge. Manufacturer and battery data     \
1239                                    shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V     \
1240                                    logic circuitry. Pull-up resistors will be provided by the charger.    \
1241                                    SMBus circuitry shall respond to a SMBus query within 2 seconds.       \
1242                                    Smart batteries may act as master or slave on the bus, but must        \
1243                                    perform bus master timing arbitration according to the SMBus           \
1244                                   specification when acting as master.                                    |
1245        """
1246        constant_elements = [
1247            {"register": SMBusReg.DEVICE_CHEMISTRY, "requirement": "LION"},
1248            {"register": SMBusReg.DEVICE_NAME, "requirement": "BB-2590/U"},
1249            {"register": SMBusReg.MANUFACTURER_NAME, "requirement": "TURN-AROUND FACTOR"},
1250            {"register": SMBusReg.SERIAL_NUM, "requirement": 0xFFFF},
1251            {"register": SMBusReg.MANUFACTURER_DATE, "requirement": None},
1252            {"register": SMBusReg.SPECIFICATION_INFO, "requirement": None},
1253            {"register": SMBusReg.DESIGN_VOLTAGE, "requirement": 16800},
1254            {
1255                "register": SMBusReg.DESIGN_CAPACITY,
1256                "requirement": math.floor(0.975 * CELL_CAPACITY_AH * 1000),
1257            },
1258            {"register": SMBusReg.CHARGING_VOLTAGE, "requirement": 16800},
1259            {"register": SMBusReg.CHARGING_CURRENT, "requirement": 2000},
1260            {"register": SMBusReg.MANUFACTURING_ACCESS, "requirement": 0},
1261            {"register": SMBusReg.REMAINING_TIME_ALARM, "requirement": 10},
1262            {"register": SMBusReg.REMAINING_CAPACITY, "requirement": 48},
1263            {"register": SMBusReg.AT_RATE_OK, "requirement": True},
1264            {"register": SMBusReg.AT_RATE_TIME_TO_EMPTY, "requirement": 65535},
1265            {"register": SMBusReg.AT_RATE_TIME_TO_FULL, "requirement": 65535},
1266            {"register": SMBusReg.AT_RATE, "requirement": 0},
1267            {"register": SMBusReg.BATTERY_MODE, "requirement": 0},
1268            {"register": SMBusReg.MAX_ERROR, "requirement": 0},
1269            {
1270                "register": SMBusReg.FULL_CHARGE_CAPACITY,
1271                "requirement": CELL_CAPACITY_AH * 1000,
1272            },
1273        ]
1274        test_failed = False
1275
1276        for element in constant_elements:
1277            register = element["register"]
1278            requirement = element["requirement"]
1279
1280            read_response = _smbus.read_register(register)
1281
1282            if register == SMBusReg.SERIAL_NUM:
1283                if read_response[0] != requirement:
1284                    logger.write_result_to_html_report(
1285                        f"{register.fname} had unique value of {read_response[0]} ({read_response[1]})"
1286                    )
1287                else:
1288                    logger.write_result_to_html_report(
1289                        f'<font color="#990000">{register.fname} was {read_response[0]} ({read_response[1]}) '
1290                        f"not a unique value</font>"
1291                    )
1292                    test_failed = True
1293                continue
1294
1295            if register == SMBusReg.REMAINING_CAPACITY:
1296                design_capacity = _smbus.read_register(SMBusReg.DESIGN_CAPACITY)
1297                requirement = design_capacity[0] // 2
1298                if requirement - 3 <= read_response[0] <= requirement + 3:
1299                    logger.write_result_to_html_report(f"{register.fname} had expected value of {requirement}")
1300                else:
1301                    logger.write_result_to_html_report(
1302                        f'<font color="#990000">{register.fname} was {read_response[0]} ({read_response[1]}) '
1303                        f"not {requirement}</font>"
1304                    )
1305                    test_failed = True
1306
1307            elif isinstance(requirement, int):
1308                if read_response[0] == requirement or read_response[1] == requirement.to_bytes(2, byteorder="little"):
1309                    logger.write_result_to_html_report(f"{register.fname} had expected value of {requirement}")
1310                else:
1311                    logger.write_result_to_html_report(
1312                        f'<font color="#990000">{register.fname} was {read_response[0]} ({read_response[1]}) '
1313                        f"not {requirement}</font>"
1314                    )
1315                    test_failed = True
1316            elif read_response[0] == requirement:
1317                logger.write_result_to_html_report(f"{register.fname} had expected value of {requirement}")
1318            else:
1319                logger.write_result_to_html_report(
1320                    f'<font color="#990000">{register.fname} was {read_response[0]} ({read_response[1]}) '
1321                    f"not {requirement}</font>"
1322                )
1323                test_failed = True
1324
1325        if test_failed:
1326            pytest.fail()
1327
1328        logger.write_result_to_html_report("All SMBus Constant Values passed")
Description Constant SMBus Values
GitHub Issue turnaroundfactor/HITL#385
Google Docs Google Sheet Cell
MIL-PRF Sections 3.5.9.1 (SMBus)
Instructions 1. Read Device Chemistry [0x22]
2. Read Device Name [0x21]
3. Read Manufacturer Name [0x20]
4. Read Serial Number [0x1C]
5. Read Manufacturer Date [0x1B]
6. Read Specification Info [0x1A]
7. Read Design Voltage [0x19]
8. Read Design Capacity [0x18]
9. Read Charging Voltage [0x15]
10. Read Charging Current [0x14]
11. Read Manufacturer Access [0x00]
12. Read Remaining Time Alarm [0x02]
13. Read Remaining Capacity Alarm [0x01]
14. Read AtRateOk [0x07]
15. Read AtRateTimeToEmpty [0x06]
16. Read AtRateTimeToFull [0x05]
17. Read AtRate [0x04]
18. Read Battery Mode [0x03]
19. Read Max Error [0x0C]
20. Read Full Charge Capacity [0x10]
Pass / Fail Criteria Charging (mA) ----
⦁ Expect Device Chemistry [0x22] to be LION
⦁ Expect Device Name [0x21] to be BB-2590/U
⦁ Expect Manufacturer Name [0x20] to be TURN-AROUND FACTOR
⦁ Expect Serial Number [0x1C] to be a unique value
⦁ Expect Manufacturer Date [0x1B] to be 0x0100
⦁ Expect Specification Info [0x1A] to be 0x0100
⦁ Expect Design Voltage [0x19] to be 16800
⦁ Expect Design Capacity [0x18] to be CAPACITY * 0.975
⦁ Expect Charging Voltage [0x15] to be 16800
⦁ Expect Charging Current [0x14] to be 2000
⦁ Expect Manufacturer Access [0x00] to be 0
⦁ Expect Remaining Time Alarm [0x02] to be 10
⦁ Expect Remaining Capacity Alarm [0x01] to be SOC * Design Capacity
⦁ Expect AtRateOk [0x07] to be True
⦁ Expect AtRateTimeToEmpty [0x06] to be 65535
⦁ Expect AtRateTimeToFull [0x05] to be 65535
⦁ Expect AtRate [0x04] to be 0
⦁ Expect Battery Mode [0x03] to be 0
⦁ Expect Max Error [0x0C] to be 0
⦁ Expect Full Charge Capacity [0x10] to be CAPACITY
Estimated Duration 10 seconds
Note When specified (see 3.1), batteries shall be compliant with System Management Bus (SMBus) Specification Revision 1.1 and Smart Battery Data (SBData) Specification, version 1.1, with the exception that SBData safety signal hardware requirements therein shall be replaced with a charge enable when a charge enable is specified (see 3.1 and 3.5.6). Certification is required. Batteries shall be compatible with appropriate Level 2 and Level 3 chargers (see 6.4.7). When tested as specified in 4.7.2.15.1, SMBus data output shall be accurate within +0/-5% of the actual state of charge for the battery under test throughout the discharge. Manufacturer and battery data shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V logic circuitry. Pull-up resistors will be provided by the charger. SMBus circuitry shall respond to a SMBus query within 2 seconds. Smart batteries may act as master or slave on the bus, but must perform bus master timing arbitration according to the SMBus specification when acting as master.
pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'soc': 0.5}]), kwargs={'indirect': True})]
class TestVariableSMBusValues:
1331class TestVariableSMBusValues:
1332    """Test variable SMBus values"""
1333
1334    class TestCycleCount(CSVRecordEvent):
1335        """@private Compare SMBus Cycle Count to reported count"""
1336
1337        cycle_count = 0
1338        required_value = None
1339
1340        @classmethod
1341        def failed(cls) -> bool:
1342            """Check if test parameters were exceeded"""
1343            return bool(cls.cycle_count != cls.required_value)
1344
1345        @classmethod
1346        def verify(cls, row, _serial_data, _cell_data):
1347            """Set Cycle count value"""
1348            cls.cycle_count = row["Cycle Count"]
1349            if cls.required_value is None:
1350                cls.required_value = cls.cycle_count + 1
1351
1352        @classmethod
1353        def result(cls):
1354            """Detailed test result information"""
1355            return f"Cycle Count: {cls.cmp(cls.cycle_count, '==', cls.required_value or 0, form='d')}"
1356
1357    class TestCurrent(CSVRecordEvent):
1358        """@private Compare SMBus Current value to expected value"""
1359
1360        allowable_error = 0.015
1361        smbus_data = SimpleNamespace(current=0, terminal=0, error=0.0)
1362
1363        @classmethod
1364        def failed(cls) -> bool:
1365            """Check if test parameters were exceeded"""
1366            return bool(cls.smbus_data.error > cls.allowable_error)
1367
1368        @classmethod
1369        def verify(cls, row, _serial_data, _cell_data):
1370            """Set current and expected values"""
1371            row_data = SimpleNamespace(current=row["Current (mA)"], terminal=row["HITL Current (A)"] * 1000)
1372            row_data.error = abs((row_data.current - row_data.terminal) / row_data.terminal)
1373            if abs(row_data.current) > 100:
1374                cls.smbus_data = max(cls.smbus_data, row_data, key=lambda data: data.error)
1375
1376        @classmethod
1377        def result(cls):
1378            """Detailed test result information"""
1379            return (
1380                f"Current error: {cls.cmp(cls.smbus_data.error, '<=', cls.allowable_error)} "
1381                f"(SMBus Current: {cls.smbus_data.current} mA, HITL Terminal Current: {cls.smbus_data.terminal} mA)"
1382            )
1383
1384    class TestVoltage(CSVRecordEvent):
1385        """@private Compare HITL Terminal Voltage to reported SMBus voltage"""
1386
1387        allowable_error = 0.015
1388        smbus_data = SimpleNamespace(voltage=0, terminal=0, error=0.0)
1389
1390        @classmethod
1391        def failed(cls) -> bool:
1392            """Check if test parameters were exceeded"""
1393            return bool(cls.smbus_data.error > cls.allowable_error)
1394
1395        @classmethod
1396        def verify(cls, row, _serial_data, _cell_data):
1397            """Voltage within range"""
1398            row_data = SimpleNamespace(voltage=row["Voltage (mV)"], terminal=row["HITL Voltage (V)"] * 1000)
1399            row_data.error = abs((row_data.voltage - row_data.terminal) / row_data.terminal)
1400            cls.smbus_data = max(cls.smbus_data, row_data, key=lambda data: data.error)
1401
1402        @classmethod
1403        def result(cls):
1404            """Detailed test result information."""
1405            return (
1406                f"Voltage error: {cls.cmp(cls.smbus_data.error, '<=', cls.allowable_error)} "
1407                f"(SMBus Voltage: {cls.smbus_data.voltage} mV, HITL Terminal Voltage: {cls.smbus_data.terminal} mV)"
1408            )
1409
1410    class TestTemperature(CSVRecordEvent):
1411        """@private Compare average HITL THERM1 & THERM2 temperatures to reported SMBus Temperature"""
1412
1413        smbus_temperature = 0
1414        average_hitl_temperature = 0
1415        low_temperature_range = 0
1416        high_temperature_range = 0
1417
1418        @classmethod
1419        def failed(cls) -> bool:
1420            """Check if test parameters were exceeded"""
1421            return (cls.smbus_temperature <= cls.low_temperature_range) or (
1422                cls.smbus_temperature >= cls.high_temperature_range
1423            )
1424
1425        @classmethod
1426        def verify(cls, row, _serial_data, _cell_data):
1427            """Voltage within range"""
1428            cls.smbus_temperature = row["Temperature (dK)"] / 10 - 273
1429            cls.average_hitl_temperature = statistics.mean([_plateset.thermistor1, _plateset.thermistor2])
1430            cls.low_temperature_range = cls.average_hitl_temperature - 5
1431            cls.high_temperature_range = cls.average_hitl_temperature + 5
1432
1433        @classmethod
1434        def result(cls):
1435            """Detailed test result information."""
1436            return (
1437                f"Temperature Error: "
1438                f"{cls.cmp(cls.smbus_temperature, '>=', cls.low_temperature_range, '°C', '.2f')}, "
1439                f" {cls.cmp(cls.smbus_temperature, '<=', cls.high_temperature_range, '°C', '.2f')},"
1440                f"(SMBus Temperature: {cls.smbus_temperature:.2f} °C, "
1441                f"HITL THERM1 & THERM2 Average Temperature: {cls.average_hitl_temperature:.2f} °C)"
1442            )
1443
1444    class TestCellVoltage1(CSVRecordEvent):
1445        """@private Compare HITL Cell Voltage 1 to reported Cell Voltage1"""
1446
1447        allowable_error = 0.01
1448        max = SimpleNamespace(cell_voltage=0, hitl_cell_voltage=0, error=0.0)
1449
1450        @classmethod
1451        def failed(cls) -> bool:
1452            """Check if test parameters were exceeded"""
1453            return bool(cls.max.error > cls.allowable_error)
1454
1455        @classmethod
1456        def verify(cls, row, _serial_data, _cell_data):
1457            """Cell Voltage 1 within range"""
1458            row_data = SimpleNamespace(
1459                cell_voltage=row["Cell Voltage1 (mV)"], hitl_cell_voltage=(float(row["Cell Sim 1 Volts (V)"]) * 1000)
1460            )
1461            row_data.error = abs((row_data.cell_voltage - row_data.hitl_cell_voltage) / row_data.hitl_cell_voltage)
1462            cls.max = max(cls.max, row_data, key=lambda data: data.error)
1463
1464        @classmethod
1465        def result(cls):
1466            """Detailed test result information."""
1467            return (
1468                f"Cell Voltage1 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)}"
1469                f"(SMBus Cell Voltage1: {cls.max.cell_voltage:.2f} mV, HITL Cell Sim 1 Voltage: "
1470                f"{cls.max.hitl_cell_voltage:.2f} mV)"
1471            )
1472
1473    class TestCellVoltage2(CSVRecordEvent):
1474        """@private Compare HITL Cell Voltage 2 to reported Cell Voltage2"""
1475
1476        allowable_error = 0.01
1477        max = SimpleNamespace(cell_voltage=0, hitl_cell_voltage=0, error=0.0)
1478
1479        @classmethod
1480        def failed(cls) -> bool:
1481            """Check if test parameters were exceeded"""
1482            return bool(cls.max.error > cls.allowable_error)
1483
1484        @classmethod
1485        def verify(cls, row, _serial_data, _cell_data):
1486            """Cell Voltage 2 within range"""
1487            row_data = SimpleNamespace(
1488                cell_voltage=row["Cell Voltage2 (mV)"], hitl_cell_voltage=(float(row["Cell Sim 2 Volts (V)"]) * 1000)
1489            )
1490            row_data.error = abs((row_data.cell_voltage - row_data.hitl_cell_voltage) / row_data.hitl_cell_voltage)
1491            cls.max = max(cls.max, row_data, key=lambda data: data.error)
1492
1493        @classmethod
1494        def result(cls):
1495            """Detailed test result information."""
1496            return (
1497                f"Cell Voltage2 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)}"
1498                f"(SMBus Cell Voltage2: {cls.max.cell_voltage:.2f} mV, "
1499                f"HITL Cell Sim 2 Voltage: {cls.max.hitl_cell_voltage:.2f} mV)"
1500            )
1501
1502    class TestCellVoltage3(CSVRecordEvent):
1503        """@private Compare HITL Cell Voltage 3 to reported Cell Voltage3"""
1504
1505        allowable_error = 0.01
1506        max = SimpleNamespace(cell_voltage=0, hitl_cell_voltage=0, error=0.0)
1507
1508        @classmethod
1509        def failed(cls) -> bool:
1510            """Check if test parameters were exceeded"""
1511            return bool(cls.max.error > cls.allowable_error)
1512
1513        @classmethod
1514        def verify(cls, row, _serial_data, _cell_data):
1515            """Cell Voltage 3 within range"""
1516            row_data = SimpleNamespace(
1517                cell_voltage=row["Cell Voltage3 (mV)"], hitl_cell_voltage=(float(row["Cell Sim 3 Volts (V)"]) * 1000)
1518            )
1519            row_data.error = abs((row_data.cell_voltage - row_data.hitl_cell_voltage) / row_data.hitl_cell_voltage)
1520            cls.max = max(cls.max, row_data, key=lambda data: data.error)
1521
1522        @classmethod
1523        def result(cls):
1524            """Detailed test result information."""
1525            return (
1526                f"Cell Voltage3 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)}"
1527                f"(SMBus Cell Voltage3: {cls.max.cell_voltage:.2f} mV, "
1528                f"HITL Cell Sim 3 Voltage: {cls.max.hitl_cell_voltage:.2f} mV)"
1529            )
1530
1531    class TestCellVoltage4(CSVRecordEvent):
1532        """@private Compare HITL Cell Voltage 4 to reported Cell Voltage4"""
1533
1534        allowable_error = 0.01
1535        max = SimpleNamespace(cell_voltage=0, hitl_cell_voltage=0, error=0.0)
1536
1537        @classmethod
1538        def failed(cls) -> bool:
1539            """Check if test parameters were exceeded"""
1540            return bool(cls.max.error > cls.allowable_error)
1541
1542        @classmethod
1543        def verify(cls, row, _serial_data, _cell_data):
1544            """Cell Voltage 4 within range"""
1545            row_data = SimpleNamespace(
1546                cell_voltage=row["Cell Voltage4 (mV)"], hitl_cell_voltage=(float(row["Cell Sim 4 Volts (V)"]) * 1000)
1547            )
1548            row_data.error = abs((row_data.cell_voltage - row_data.hitl_cell_voltage) / row_data.hitl_cell_voltage)
1549            cls.max = max(cls.max, row_data, key=lambda data: data.error)
1550
1551        @classmethod
1552        def result(cls):
1553            """Detailed test result information."""
1554            return (
1555                f"Cell Voltage4 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)}"
1556                f"(SMBus Cell Voltage4: {cls.max.cell_voltage:.2f} mV, "
1557                f"HITL Cell Sim 4 Voltage: {cls.max.hitl_cell_voltage:.2f} mV)"
1558            )
1559
1560    class TestStateOfCharge(CSVRecordEvent):
1561        """@private Compare HITL State of Charge to reported SMBus State of Charge"""
1562
1563        allowable_error = 5
1564        max = SimpleNamespace(absolute_charge=0, hitl_charge=0, error=0.0)
1565
1566        @classmethod
1567        def failed(cls) -> bool:
1568            """Check if test parameters were exceeded"""
1569            return bool(cls.max.error > cls.allowable_error)
1570
1571        @classmethod
1572        def verify(cls, row, _serial_data, _cell_data):
1573            """State of Charge within range"""
1574            row_data = SimpleNamespace(absolute_charge=row["Absolute State Of Charge (%)"])
1575            row_data.hitl_charge = min(
1576                float(row["Cell Sim 1 SOC (%)"].replace("%", "")),
1577                float(row["Cell Sim 2 SOC (%)"].replace("%", "")),
1578                float(row["Cell Sim 3 SOC (%)"].replace("%", "")),
1579                float(row["Cell Sim 4 SOC (%)"].replace("%", "")),
1580            )
1581            row_data.error = abs((row_data.absolute_charge - row_data.hitl_charge))
1582            cls.max = max(cls.max, row_data, key=lambda data: data.error)
1583
1584        @classmethod
1585        def result(cls):
1586            """Detailed test result information."""
1587            return (
1588                f"State of Charge error: {cls.cmp(cls.max.error, '<=', cls.allowable_error, '%', '.1f')}"
1589                f"(SMBus Absolute State of Charge: {cls.max.absolute_charge:.2f} %, "
1590                f"Lowest HITL State of Charge: {cls.max.hitl_charge:.2f} %)"
1591            )
1592
1593    def test_variable_smbus_values(self):
1594        """
1595           | Description          | Variable SMBus Values                                                  |
1596           | :------------------- | :--------------------------------------------------------------------- |
1597           | GitHub Issue         | turnaroundfactor/HITL#385                                       |
1598           | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1599jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D12) |
1600           | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
1601           | Instructions         | 1. Set thermistors to 23°C                                        </br>\
1602                                    2. Put cells in a rested state at 2.5V per cell                   </br>\
1603                                    3. Charge battery (16.8V / 3A / 100 mA cutoff)                    </br>\
1604                                    4. Rest for 3.1 hours (we want to calculate SoH change which requires 3 hours)</br>\
1605                                    5. Discharge at 2A until battery reaches 10V then stop discharging     |
1606           | Pass / Fail Criteria | Charging (mA) ----                                                </br>\
1607                                    ⦁ Expect Cycle Count [0x17] to be 0                               </br>\
1608                                    ⦁ Expect Current [0x0A] to be within 1% for abs(Terminal Current > 100mA) </br>\
1609                                    ⦁ Expect Voltage [0x09] to be HITL Terminal Voltage within 1%     </br>\
1610                                    ⦁ Expect Temperature [0x08] to be average of HITL THERM1 & THERM2 within 5°C </br>\
1611                                    ⦁ Expect Cell Voltage1 [0x3C] to be HITL Cell Sim 1 Volts (V)     </br>\
1612                                    ⦁ Expect Cell Voltage2 [0x3D] to be HITL Cell Sim 2 Volts (V)     </br>\
1613                                    ⦁ Expect Cell Voltage3 [0x3E] to be HITL Cell Sim 3 Volts (V)     </br>\
1614                                    ⦁ Expect Cell Voltage4 [0x3F] to be HITL Cell Sim 4 Volts (V)     </br>\
1615                                    ⦁ Expect State of Charge [0x4F] to be HITL State of Charge             |
1616           | Estimated Duration   | 18 minutes                                                             |
1617           | Note                 | When specified (see 3.1), batteries shall be compliant with System     \
1618                                    Management Bus (SMBus) Specification Revision 1.1 and Smart Battery    \
1619                                    Data (SBData) Specification, version 1.1, with the exception that      \
1620                                    SBData safety signal hardware requirements therein shall be replaced   \
1621                                    with a charge enable when a charge enable is specified (see 3.1 and    \
1622                                    3.5.6). Certification is required. Batteries shall be compatible       \
1623                                    with appropriate Level 2 and Level 3 chargers (see 6.4.7).             \
1624                                    When tested as specified in 4.7.2.15.1, SMBus data output shall be     \
1625                                    accurate within +0/-5% of the actual state of charge for the battery   \
1626                                    under test throughout the discharge. Manufacturer and battery data     \
1627                                    shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V     \
1628                                    logic circuitry. Pull-up resistors will be provided by the charger.    \
1629                                    SMBus circuitry shall respond to a SMBus query within 2 seconds.       \
1630                                    Smart batteries may act as master or slave on the bus, but must        \
1631                                    perform bus master timing arbitration according to the SMBus           \
1632                                   specification when acting as master.                                    |
1633        """
1634
1635        standard_charge()
1636        standard_rest(seconds=30 if FAST_MODE else 3.1 * 3600)
1637        standard_discharge()
1638
1639        # Check results
1640        if CSVRecordEvent.failed():
1641            pytest.fail(CSVRecordEvent.result())

Test variable SMBus values

def test_variable_smbus_values(self):
1593    def test_variable_smbus_values(self):
1594        """
1595           | Description          | Variable SMBus Values                                                  |
1596           | :------------------- | :--------------------------------------------------------------------- |
1597           | GitHub Issue         | turnaroundfactor/HITL#385                                       |
1598           | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1599jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D12) |
1600           | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
1601           | Instructions         | 1. Set thermistors to 23°C                                        </br>\
1602                                    2. Put cells in a rested state at 2.5V per cell                   </br>\
1603                                    3. Charge battery (16.8V / 3A / 100 mA cutoff)                    </br>\
1604                                    4. Rest for 3.1 hours (we want to calculate SoH change which requires 3 hours)</br>\
1605                                    5. Discharge at 2A until battery reaches 10V then stop discharging     |
1606           | Pass / Fail Criteria | Charging (mA) ----                                                </br>\
1607                                    ⦁ Expect Cycle Count [0x17] to be 0                               </br>\
1608                                    ⦁ Expect Current [0x0A] to be within 1% for abs(Terminal Current > 100mA) </br>\
1609                                    ⦁ Expect Voltage [0x09] to be HITL Terminal Voltage within 1%     </br>\
1610                                    ⦁ Expect Temperature [0x08] to be average of HITL THERM1 & THERM2 within 5°C </br>\
1611                                    ⦁ Expect Cell Voltage1 [0x3C] to be HITL Cell Sim 1 Volts (V)     </br>\
1612                                    ⦁ Expect Cell Voltage2 [0x3D] to be HITL Cell Sim 2 Volts (V)     </br>\
1613                                    ⦁ Expect Cell Voltage3 [0x3E] to be HITL Cell Sim 3 Volts (V)     </br>\
1614                                    ⦁ Expect Cell Voltage4 [0x3F] to be HITL Cell Sim 4 Volts (V)     </br>\
1615                                    ⦁ Expect State of Charge [0x4F] to be HITL State of Charge             |
1616           | Estimated Duration   | 18 minutes                                                             |
1617           | Note                 | When specified (see 3.1), batteries shall be compliant with System     \
1618                                    Management Bus (SMBus) Specification Revision 1.1 and Smart Battery    \
1619                                    Data (SBData) Specification, version 1.1, with the exception that      \
1620                                    SBData safety signal hardware requirements therein shall be replaced   \
1621                                    with a charge enable when a charge enable is specified (see 3.1 and    \
1622                                    3.5.6). Certification is required. Batteries shall be compatible       \
1623                                    with appropriate Level 2 and Level 3 chargers (see 6.4.7).             \
1624                                    When tested as specified in 4.7.2.15.1, SMBus data output shall be     \
1625                                    accurate within +0/-5% of the actual state of charge for the battery   \
1626                                    under test throughout the discharge. Manufacturer and battery data     \
1627                                    shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V     \
1628                                    logic circuitry. Pull-up resistors will be provided by the charger.    \
1629                                    SMBus circuitry shall respond to a SMBus query within 2 seconds.       \
1630                                    Smart batteries may act as master or slave on the bus, but must        \
1631                                    perform bus master timing arbitration according to the SMBus           \
1632                                   specification when acting as master.                                    |
1633        """
1634
1635        standard_charge()
1636        standard_rest(seconds=30 if FAST_MODE else 3.1 * 3600)
1637        standard_discharge()
1638
1639        # Check results
1640        if CSVRecordEvent.failed():
1641            pytest.fail(CSVRecordEvent.result())
Description Variable SMBus Values
GitHub Issue turnaroundfactor/HITL#385
Google Docs Google Sheet Cell
MIL-PRF Sections 3.5.9.1 (SMBus)
Instructions 1. Set thermistors to 23°C
2. Put cells in a rested state at 2.5V per cell
3. Charge battery (16.8V / 3A / 100 mA cutoff)
4. Rest for 3.1 hours (we want to calculate SoH change which requires 3 hours)
5. Discharge at 2A until battery reaches 10V then stop discharging
Pass / Fail Criteria Charging (mA) ----
⦁ Expect Cycle Count [0x17] to be 0
⦁ Expect Current [0x0A] to be within 1% for abs(Terminal Current > 100mA)
⦁ Expect Voltage [0x09] to be HITL Terminal Voltage within 1%
⦁ Expect Temperature [0x08] to be average of HITL THERM1 & THERM2 within 5°C
⦁ Expect Cell Voltage1 [0x3C] to be HITL Cell Sim 1 Volts (V)
⦁ Expect Cell Voltage2 [0x3D] to be HITL Cell Sim 2 Volts (V)
⦁ Expect Cell Voltage3 [0x3E] to be HITL Cell Sim 3 Volts (V)
⦁ Expect Cell Voltage4 [0x3F] to be HITL Cell Sim 4 Volts (V)
⦁ Expect State of Charge [0x4F] to be HITL State of Charge
Estimated Duration 18 minutes
Note When specified (see 3.1), batteries shall be compliant with System Management Bus (SMBus) Specification Revision 1.1 and Smart Battery Data (SBData) Specification, version 1.1, with the exception that SBData safety signal hardware requirements therein shall be replaced with a charge enable when a charge enable is specified (see 3.1 and 3.5.6). Certification is required. Batteries shall be compatible with appropriate Level 2 and Level 3 chargers (see 6.4.7). When tested as specified in 4.7.2.15.1, SMBus data output shall be accurate within +0/-5% of the actual state of charge for the battery under test throughout the discharge. Manufacturer and battery data shall be correctly programmed (see 4.7.2.15.1). SMBus shall use 5V logic circuitry. Pull-up resistors will be provided by the charger. SMBus circuitry shall respond to a SMBus query within 2 seconds. Smart batteries may act as master or slave on the bus, but must perform bus master timing arbitration according to the SMBus specification when acting as master.
@pytest.mark.parametrize('reset_test_environment', [{'volts': 4}], indirect=True)
class TestChargeAcceptance(hitl_tester.modules.bms.test_handler.CSVRecordEvent):
1644@pytest.mark.parametrize("reset_test_environment", [{"volts": 4}], indirect=True)
1645class TestChargeAcceptance(CSVRecordEvent):
1646    """Run a test for charge acceptance"""
1647
1648    def test_charge_acceptance(self):
1649        """
1650        | Description          | Charge Acceptance                                                      |
1651        | :------------------- | :--------------------------------------------------------------------- |
1652        | GitHub Issue         | turnaroundfactor/HITL#517                                       |
1653        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1654jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D29) |
1655        | MIL-PRF Sections     | 3.5.10.1 (Charge Acceptance)                                           |
1656        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
1657                                 2. Put cells in rested state at 4.2V per cell                     </br>\
1658                                 3. Set THERM1 and THERM2 to -20°C                                 </br>\
1659                                 4. Attempt to charge at 1A                                        </br>\
1660                                 5. Attempt to discharge at 1A                                     </br>\
1661                                 6. Set THERM1 and THERM2 to 50°C                                  </br>\
1662                                 7. Attempt to charge at 1A                                        </br>\
1663                                 8. Attempt to discharge at 1A                                          |
1664        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be -20°C +/- 1.1°C            </br>\
1665                                 ⦁ Expect HITL Terminal Current to be 1A +/- 30mA                  </br>\
1666                                 ⦁ Expect HITL Terminal Current to be -1A +/- 30mA                 </br>\
1667                                 ⦁ Expect Serial THERM1 & THERM 2 to be 50°C +/- 1.1°C             </br>\
1668                                 ⦁ Expect HITL Terminal Current to be 1A +/- 30mA                  </br>\
1669                                 ⦁ Expect HITL Terminal Current to be -1A +/- 30mA                      |
1670        | Estimated Duration   | 17 seconds                                                              |
1671        | Note                 | When tested as specified in 4.7.2.9, batteries shall meet the          \
1672                                 capacity requirements 3.5.3 and the visual mechanical requirements     \
1673                                 of TABLE XIV. The surface temperature of the battery shall not         \
1674                                 exceed 185°F (85°C)                                                    |
1675        """
1676
1677        failed_tests = []
1678        temperatures = [-20, 50]
1679
1680        for set_temp in temperatures:
1681            logger.write_info_to_report(f"Setting THERM1 & THERM2 to {set_temp}°C")
1682
1683            _plateset.disengage_safety_protocols = True
1684            _plateset.thermistor1 = _plateset.thermistor2 = set_temp
1685            _plateset.disengage_safety_protocols = False
1686
1687            time.sleep(2)
1688
1689            # Get the serial data
1690            serial_data = serial_monitor.read()
1691
1692            # Convert temperature to Celsius from Kelvin
1693            therm_one = serial_data["dk_temp"] / 10 - 273
1694            therm_two = serial_data["dk_temp1"] / 10 - 273
1695            temp_range = f"{set_temp}°C +/- 1.1°C"
1696            low_range = set_temp - 1.1
1697            high_range = set_temp + 1.1
1698
1699            if low_range <= therm_one <= high_range:
1700                logger.write_result_to_html_report(
1701                    f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
1702                )
1703            else:
1704                logger.write_result_to_html_report(
1705                    f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
1706                    f"of {temp_range}</font>"
1707                )
1708                failed_tests.append("THERM1")
1709
1710            if low_range <= therm_two <= high_range:
1711                logger.write_result_to_html_report(
1712                    f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
1713                )
1714            else:
1715                logger.write_result_to_html_report(
1716                    f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
1717                    f"expected range of {temp_range}</font>"
1718                )
1719                failed_tests.append("THERM2")
1720
1721            logger.write_info_to_report("Attempting to charge at 1A")
1722            limit = 0.03
1723            with _bms.charger(16.8, 1):
1724                time.sleep(1)
1725                charger_amps = _bms.charger.amps
1726                low_range = 1 - limit
1727                high_range = 1 + limit
1728                expected_current_range = f"1A +/- {limit}A"
1729                if low_range <= charger_amps <= high_range:
1730                    logger.write_result_to_html_report(
1731                        f"HITL Terminal Current was {charger_amps:.3f}A after charging, which was within the "
1732                        f"expected range of {expected_current_range}"
1733                    )
1734                else:
1735                    logger.write_result_to_html_report(
1736                        f'<font color="#990000">HITL Terminal Current was {charger_amps:.3f}A after charging, '
1737                        f"which was not within the expected range of {expected_current_range} </font>"
1738                    )
1739                    failed_tests.append("HITL Terminal Current")
1740
1741            logger.write_info_to_report("Attempting to discharge at 1A")
1742            with _bms.load(1):
1743                time.sleep(1)
1744                load_amps = -1 * _bms.load.amps
1745                low_range = -1 - limit
1746                high_range = -1 + limit
1747                expected_current_range = f"-1A +/- {limit}A"
1748                if low_range <= load_amps <= high_range:
1749                    logger.write_result_to_html_report(
1750                        f"HITL Terminal Current was {load_amps:.3f}A after discharging, which was within the "
1751                        f"expected range of {expected_current_range}"
1752                    )
1753                else:
1754                    logger.write_result_to_html_report(
1755                        f'<font color="#990000">HITL Terminal Current was {load_amps:.3f}A after discharging, '
1756                        f"which was not within the expected range of {expected_current_range} </font>"
1757                    )
1758                    failed_tests.append("HITL Terminal Current")
1759
1760        if len(failed_tests) > 0:
1761            pytest.fail()
1762
1763        logger.write_result_to_html_report("All checks passed test")

Run a test for charge acceptance

def test_charge_acceptance(self):
1648    def test_charge_acceptance(self):
1649        """
1650        | Description          | Charge Acceptance                                                      |
1651        | :------------------- | :--------------------------------------------------------------------- |
1652        | GitHub Issue         | turnaroundfactor/HITL#517                                       |
1653        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1654jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D29) |
1655        | MIL-PRF Sections     | 3.5.10.1 (Charge Acceptance)                                           |
1656        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
1657                                 2. Put cells in rested state at 4.2V per cell                     </br>\
1658                                 3. Set THERM1 and THERM2 to -20°C                                 </br>\
1659                                 4. Attempt to charge at 1A                                        </br>\
1660                                 5. Attempt to discharge at 1A                                     </br>\
1661                                 6. Set THERM1 and THERM2 to 50°C                                  </br>\
1662                                 7. Attempt to charge at 1A                                        </br>\
1663                                 8. Attempt to discharge at 1A                                          |
1664        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be -20°C +/- 1.1°C            </br>\
1665                                 ⦁ Expect HITL Terminal Current to be 1A +/- 30mA                  </br>\
1666                                 ⦁ Expect HITL Terminal Current to be -1A +/- 30mA                 </br>\
1667                                 ⦁ Expect Serial THERM1 & THERM 2 to be 50°C +/- 1.1°C             </br>\
1668                                 ⦁ Expect HITL Terminal Current to be 1A +/- 30mA                  </br>\
1669                                 ⦁ Expect HITL Terminal Current to be -1A +/- 30mA                      |
1670        | Estimated Duration   | 17 seconds                                                              |
1671        | Note                 | When tested as specified in 4.7.2.9, batteries shall meet the          \
1672                                 capacity requirements 3.5.3 and the visual mechanical requirements     \
1673                                 of TABLE XIV. The surface temperature of the battery shall not         \
1674                                 exceed 185°F (85°C)                                                    |
1675        """
1676
1677        failed_tests = []
1678        temperatures = [-20, 50]
1679
1680        for set_temp in temperatures:
1681            logger.write_info_to_report(f"Setting THERM1 & THERM2 to {set_temp}°C")
1682
1683            _plateset.disengage_safety_protocols = True
1684            _plateset.thermistor1 = _plateset.thermistor2 = set_temp
1685            _plateset.disengage_safety_protocols = False
1686
1687            time.sleep(2)
1688
1689            # Get the serial data
1690            serial_data = serial_monitor.read()
1691
1692            # Convert temperature to Celsius from Kelvin
1693            therm_one = serial_data["dk_temp"] / 10 - 273
1694            therm_two = serial_data["dk_temp1"] / 10 - 273
1695            temp_range = f"{set_temp}°C +/- 1.1°C"
1696            low_range = set_temp - 1.1
1697            high_range = set_temp + 1.1
1698
1699            if low_range <= therm_one <= high_range:
1700                logger.write_result_to_html_report(
1701                    f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
1702                )
1703            else:
1704                logger.write_result_to_html_report(
1705                    f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
1706                    f"of {temp_range}</font>"
1707                )
1708                failed_tests.append("THERM1")
1709
1710            if low_range <= therm_two <= high_range:
1711                logger.write_result_to_html_report(
1712                    f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
1713                )
1714            else:
1715                logger.write_result_to_html_report(
1716                    f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
1717                    f"expected range of {temp_range}</font>"
1718                )
1719                failed_tests.append("THERM2")
1720
1721            logger.write_info_to_report("Attempting to charge at 1A")
1722            limit = 0.03
1723            with _bms.charger(16.8, 1):
1724                time.sleep(1)
1725                charger_amps = _bms.charger.amps
1726                low_range = 1 - limit
1727                high_range = 1 + limit
1728                expected_current_range = f"1A +/- {limit}A"
1729                if low_range <= charger_amps <= high_range:
1730                    logger.write_result_to_html_report(
1731                        f"HITL Terminal Current was {charger_amps:.3f}A after charging, which was within the "
1732                        f"expected range of {expected_current_range}"
1733                    )
1734                else:
1735                    logger.write_result_to_html_report(
1736                        f'<font color="#990000">HITL Terminal Current was {charger_amps:.3f}A after charging, '
1737                        f"which was not within the expected range of {expected_current_range} </font>"
1738                    )
1739                    failed_tests.append("HITL Terminal Current")
1740
1741            logger.write_info_to_report("Attempting to discharge at 1A")
1742            with _bms.load(1):
1743                time.sleep(1)
1744                load_amps = -1 * _bms.load.amps
1745                low_range = -1 - limit
1746                high_range = -1 + limit
1747                expected_current_range = f"-1A +/- {limit}A"
1748                if low_range <= load_amps <= high_range:
1749                    logger.write_result_to_html_report(
1750                        f"HITL Terminal Current was {load_amps:.3f}A after discharging, which was within the "
1751                        f"expected range of {expected_current_range}"
1752                    )
1753                else:
1754                    logger.write_result_to_html_report(
1755                        f'<font color="#990000">HITL Terminal Current was {load_amps:.3f}A after discharging, '
1756                        f"which was not within the expected range of {expected_current_range} </font>"
1757                    )
1758                    failed_tests.append("HITL Terminal Current")
1759
1760        if len(failed_tests) > 0:
1761            pytest.fail()
1762
1763        logger.write_result_to_html_report("All checks passed test")
Description Charge Acceptance
GitHub Issue turnaroundfactor/HITL#517
Google Docs Google Sheet Cell
MIL-PRF Sections 3.5.10.1 (Charge Acceptance)
Instructions 1. Set THERM1 and THERM2 to 23°C
2. Put cells in rested state at 4.2V per cell
3. Set THERM1 and THERM2 to -20°C
4. Attempt to charge at 1A
5. Attempt to discharge at 1A
6. Set THERM1 and THERM2 to 50°C
7. Attempt to charge at 1A
8. Attempt to discharge at 1A
Pass / Fail Criteria ⦁ Expect Serial THERM1 & THERM 2 to be -20°C +/- 1.1°C
⦁ Expect HITL Terminal Current to be 1A +/- 30mA
⦁ Expect HITL Terminal Current to be -1A +/- 30mA
⦁ Expect Serial THERM1 & THERM 2 to be 50°C +/- 1.1°C
⦁ Expect HITL Terminal Current to be 1A +/- 30mA
⦁ Expect HITL Terminal Current to be -1A +/- 30mA
Estimated Duration 17 seconds
Note When tested as specified in 4.7.2.9, batteries shall meet the capacity requirements 3.5.3 and the visual mechanical requirements of TABLE XIV. The surface temperature of the battery shall not exceed 185°F (85°C)
pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 4}]), kwargs={'indirect': True})]
@pytest.mark.parametrize('reset_test_environment', [{'volts': 4.2}], indirect=True)
class TestHighTemperaturePermanentCutoff(hitl_tester.modules.bms.test_handler.CSVRecordEvent):
1766@pytest.mark.parametrize("reset_test_environment", [{"volts": 4.2}], indirect=True)
1767class TestHighTemperaturePermanentCutoff(CSVRecordEvent):
1768    """Run a test for High temperature permanent cutoff"""
1769
1770    def test_high_temp_perm_cutoff(self):
1771        """
1772        | Description          | High Temperature Permanent Cutoff                                      |
1773        | :------------------- | :--------------------------------------------------------------------- |
1774        | GitHub Issue         | turnaroundfactor/HITL#516                                       |
1775        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1776jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D28) |
1777        | MIL-PRF Sections     | 3.7.2.5 (High Temperature permanent cut off devices)                   |
1778        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
1779                                 2. Put cells in rested state at 4.2V per cell                     </br>\
1780                                 3. Set THERM1 and THERM2 to 95°C                                  </br>\
1781                                 4. Measure Voltage                                                </br>\
1782                                 5. Attempt to charge at 1A                                        </br>\
1783                                 6. Attempt to discharge at 1A                                     </br>\
1784                                 7. Set temperature to 40°C                                        </br>\
1785                                 8. Measure Voltage                                                     |
1786        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 95°C +/- 5°C               </br>\
1787                                 ⦁ Expect Overtemperature Permanent Disable flag to be set         </br>\
1788                                 ⦁ HITL Terminal Voltage is 0V +/- 0.2 V                           </br>\
1789                                 ⦁ HITL Terminal Current is 0A +/- 1mA after charging            </br>\
1790                                 ⦁ HITL Terminal Current is 0A +/- 1mA after discharging         </br>\
1791                                 ⦁ HITL Terminal Voltage is 0V +/- 0.2 V                                |
1792        | Estimated Duration   | 6 seconds                                                              |
1793        | Note                 | The following test shall be performed. Charge batteries as specified   \
1794                                 in 4.6; use of 4.6.3 is permitted. Each battery shall be permanently   \
1795                                 shut off when the temperature of the battery reaches 199 ± 9°F         \
1796                                 (93 ± 5°C). The device shall prevent charging and discharging of the   \
1797                                 battery. The minimum quantity shall be as specified in the applicable  \
1798                                 specification sheet. When tested as specified in 4.7.4.10, battery     \
1799                                 voltage shall be zero volts after high temperature storage and shall   \
1800                                 remain at zero after 104°F (40°C) storage.                             |
1801        """
1802
1803        failed_tests = []
1804        timeout_seconds = 30
1805
1806        logger.write_info_to_report("Setting THERM1 & THERM2 to 95°C")
1807        _plateset.disengage_safety_protocols = True
1808        _plateset.thermistor1 = _plateset.thermistor2 = 95
1809        _plateset.disengage_safety_protocols = False
1810
1811        time.sleep(1)
1812        serial_data = serial_monitor.read()  # Get the serial data
1813        # Convert temperature to Celsius from Kelvin
1814        therm_one = serial_data["dk_temp"] / 10 - 273
1815        therm_two = serial_data["dk_temp1"] / 10 - 273
1816
1817        if 90 <= therm_one <= 100:
1818            logger.write_result_to_html_report(
1819                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of 95°C +/- 5°C"
1820            )
1821        else:
1822            logger.write_result_to_html_report(
1823                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
1824                f"of 95°C +/- 5°C</font>"
1825            )
1826            failed_tests.append("THERM1")
1827
1828        if 90 <= therm_two <= 100:
1829            logger.write_result_to_html_report(
1830                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of 95°C +/- 5°C"
1831            )
1832        else:
1833            logger.write_result_to_html_report(
1834                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
1835                f"expected range of 95°C +/- 5°C</font>"
1836            )
1837            failed_tests.append("THERM2")
1838
1839        start = time.perf_counter()
1840        while (serial_data := serial_monitor.read()) and not serial_data["flags.permanentdisable_overtemp"]:
1841            if time.perf_counter() - start > timeout_seconds:
1842                message = f"Over-temperature permanent disable was not raised after {timeout_seconds} seconds."
1843                logger.write_failure_to_html_report(message)
1844                failed_tests.append("Over-temperature permanent disable flag.")
1845                break
1846        else:
1847            logger.write_result_to_html_report("Over-temperature permanent disable was properly set.")
1848
1849        logger.write_info_to_report("Measuring voltage...")
1850        time.sleep(1)
1851
1852        volt_range = "0V (-0.2V / +3V)"
1853
1854        with _bms.load(0.0, DischargeType.CONSTANT_RESISTANCE, 50_000):
1855            if not -0.2 <= _bms.dmm.volts <= 3:
1856                logger.write_failure_to_html_report(
1857                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
1858                    f"which was not within the expected range of {volt_range}"
1859                )
1860                failed_tests.append("HITL Terminal Voltage after temperature was set to 95°C")
1861            else:
1862                logger.write_result_to_html_report(
1863                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
1864                    f"which was within the expected range of {volt_range}"
1865                )
1866
1867        logger.write_info_to_report("Attempting to charge at 1A")
1868        with _bms.charger(16.8, 1.0):
1869            time.sleep(1)
1870            if -0.020 <= _bms.charger.amps <= 0.020:
1871                logger.write_result_to_html_report(
1872                    f"HITL Terminal Current was {_bms.charger.amps:.3f}A, which was within the "
1873                    f"expected range of 0A +/- 20mA"
1874                )
1875            else:
1876                logger.write_result_to_html_report(
1877                    f'<font color="#990000">HITL Terminal Current was {_bms.charger.amps:.3f}A, which was'
1878                    f" not within the expected range of 0A +/- 20mA </font>"
1879                )
1880                failed_tests.append("HITL Terminal Current after attempting to charge at 1A")
1881
1882        logger.write_info_to_report("Attempting to discharge at 1A")
1883
1884        with _bms.load(1):
1885            time.sleep(1)
1886            if -0.020 <= _bms.load.amps <= 0.020:
1887                logger.write_result_to_html_report(
1888                    f"HITL Terminal Current was {_bms.load.amps:.3f}A, which was within the "
1889                    f"expected range of 0A +/- 20mA"
1890                )
1891            else:
1892                logger.write_result_to_html_report(
1893                    f'<font color="#990000">HITL Terminal Current was {_bms.load.amps:.3f}A, '
1894                    f"which was not within the expected range of 0A +/- 20mA </font>"
1895                )
1896                failed_tests.append("HITL Terminal Current after attempting to discharge at 1A")
1897
1898        logger.write_info_to_report("Setting Temperature to 40°C")
1899        _plateset.thermistor1 = _plateset.thermistor2 = 40
1900
1901        logger.write_info_to_report("Measuring voltage...")
1902        time.sleep(1)
1903        with _bms.load(0.0, DischargeType.CONSTANT_RESISTANCE, 50_000):
1904            if not -0.2 <= _bms.dmm.volts <= 3:
1905                logger.write_failure_to_html_report(
1906                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
1907                    f"which was not within the expected range of {volt_range}"
1908                )
1909                failed_tests.append("HITL Terminal Voltage after temperature was set to 40°C")
1910            else:
1911                logger.write_result_to_html_report(
1912                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V,"
1913                    f"which was within the expected range of {volt_range}"
1914                )
1915
1916        if len(failed_tests) > 0:
1917            message = f"Overall, the following checks failed: {', '.join(failed_tests)}"
1918            logger.write_result_to_html_report(f'<font color="#990000">{message}</font>')
1919            pytest.fail(message)
1920
1921        logger.write_result_to_html_report("All checks passed test")

Run a test for High temperature permanent cutoff

def test_high_temp_perm_cutoff(self):
1770    def test_high_temp_perm_cutoff(self):
1771        """
1772        | Description          | High Temperature Permanent Cutoff                                      |
1773        | :------------------- | :--------------------------------------------------------------------- |
1774        | GitHub Issue         | turnaroundfactor/HITL#516                                       |
1775        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1776jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D28) |
1777        | MIL-PRF Sections     | 3.7.2.5 (High Temperature permanent cut off devices)                   |
1778        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
1779                                 2. Put cells in rested state at 4.2V per cell                     </br>\
1780                                 3. Set THERM1 and THERM2 to 95°C                                  </br>\
1781                                 4. Measure Voltage                                                </br>\
1782                                 5. Attempt to charge at 1A                                        </br>\
1783                                 6. Attempt to discharge at 1A                                     </br>\
1784                                 7. Set temperature to 40°C                                        </br>\
1785                                 8. Measure Voltage                                                     |
1786        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 95°C +/- 5°C               </br>\
1787                                 ⦁ Expect Overtemperature Permanent Disable flag to be set         </br>\
1788                                 ⦁ HITL Terminal Voltage is 0V +/- 0.2 V                           </br>\
1789                                 ⦁ HITL Terminal Current is 0A +/- 1mA after charging            </br>\
1790                                 ⦁ HITL Terminal Current is 0A +/- 1mA after discharging         </br>\
1791                                 ⦁ HITL Terminal Voltage is 0V +/- 0.2 V                                |
1792        | Estimated Duration   | 6 seconds                                                              |
1793        | Note                 | The following test shall be performed. Charge batteries as specified   \
1794                                 in 4.6; use of 4.6.3 is permitted. Each battery shall be permanently   \
1795                                 shut off when the temperature of the battery reaches 199 ± 9°F         \
1796                                 (93 ± 5°C). The device shall prevent charging and discharging of the   \
1797                                 battery. The minimum quantity shall be as specified in the applicable  \
1798                                 specification sheet. When tested as specified in 4.7.4.10, battery     \
1799                                 voltage shall be zero volts after high temperature storage and shall   \
1800                                 remain at zero after 104°F (40°C) storage.                             |
1801        """
1802
1803        failed_tests = []
1804        timeout_seconds = 30
1805
1806        logger.write_info_to_report("Setting THERM1 & THERM2 to 95°C")
1807        _plateset.disengage_safety_protocols = True
1808        _plateset.thermistor1 = _plateset.thermistor2 = 95
1809        _plateset.disengage_safety_protocols = False
1810
1811        time.sleep(1)
1812        serial_data = serial_monitor.read()  # Get the serial data
1813        # Convert temperature to Celsius from Kelvin
1814        therm_one = serial_data["dk_temp"] / 10 - 273
1815        therm_two = serial_data["dk_temp1"] / 10 - 273
1816
1817        if 90 <= therm_one <= 100:
1818            logger.write_result_to_html_report(
1819                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of 95°C +/- 5°C"
1820            )
1821        else:
1822            logger.write_result_to_html_report(
1823                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
1824                f"of 95°C +/- 5°C</font>"
1825            )
1826            failed_tests.append("THERM1")
1827
1828        if 90 <= therm_two <= 100:
1829            logger.write_result_to_html_report(
1830                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of 95°C +/- 5°C"
1831            )
1832        else:
1833            logger.write_result_to_html_report(
1834                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
1835                f"expected range of 95°C +/- 5°C</font>"
1836            )
1837            failed_tests.append("THERM2")
1838
1839        start = time.perf_counter()
1840        while (serial_data := serial_monitor.read()) and not serial_data["flags.permanentdisable_overtemp"]:
1841            if time.perf_counter() - start > timeout_seconds:
1842                message = f"Over-temperature permanent disable was not raised after {timeout_seconds} seconds."
1843                logger.write_failure_to_html_report(message)
1844                failed_tests.append("Over-temperature permanent disable flag.")
1845                break
1846        else:
1847            logger.write_result_to_html_report("Over-temperature permanent disable was properly set.")
1848
1849        logger.write_info_to_report("Measuring voltage...")
1850        time.sleep(1)
1851
1852        volt_range = "0V (-0.2V / +3V)"
1853
1854        with _bms.load(0.0, DischargeType.CONSTANT_RESISTANCE, 50_000):
1855            if not -0.2 <= _bms.dmm.volts <= 3:
1856                logger.write_failure_to_html_report(
1857                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
1858                    f"which was not within the expected range of {volt_range}"
1859                )
1860                failed_tests.append("HITL Terminal Voltage after temperature was set to 95°C")
1861            else:
1862                logger.write_result_to_html_report(
1863                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
1864                    f"which was within the expected range of {volt_range}"
1865                )
1866
1867        logger.write_info_to_report("Attempting to charge at 1A")
1868        with _bms.charger(16.8, 1.0):
1869            time.sleep(1)
1870            if -0.020 <= _bms.charger.amps <= 0.020:
1871                logger.write_result_to_html_report(
1872                    f"HITL Terminal Current was {_bms.charger.amps:.3f}A, which was within the "
1873                    f"expected range of 0A +/- 20mA"
1874                )
1875            else:
1876                logger.write_result_to_html_report(
1877                    f'<font color="#990000">HITL Terminal Current was {_bms.charger.amps:.3f}A, which was'
1878                    f" not within the expected range of 0A +/- 20mA </font>"
1879                )
1880                failed_tests.append("HITL Terminal Current after attempting to charge at 1A")
1881
1882        logger.write_info_to_report("Attempting to discharge at 1A")
1883
1884        with _bms.load(1):
1885            time.sleep(1)
1886            if -0.020 <= _bms.load.amps <= 0.020:
1887                logger.write_result_to_html_report(
1888                    f"HITL Terminal Current was {_bms.load.amps:.3f}A, which was within the "
1889                    f"expected range of 0A +/- 20mA"
1890                )
1891            else:
1892                logger.write_result_to_html_report(
1893                    f'<font color="#990000">HITL Terminal Current was {_bms.load.amps:.3f}A, '
1894                    f"which was not within the expected range of 0A +/- 20mA </font>"
1895                )
1896                failed_tests.append("HITL Terminal Current after attempting to discharge at 1A")
1897
1898        logger.write_info_to_report("Setting Temperature to 40°C")
1899        _plateset.thermistor1 = _plateset.thermistor2 = 40
1900
1901        logger.write_info_to_report("Measuring voltage...")
1902        time.sleep(1)
1903        with _bms.load(0.0, DischargeType.CONSTANT_RESISTANCE, 50_000):
1904            if not -0.2 <= _bms.dmm.volts <= 3:
1905                logger.write_failure_to_html_report(
1906                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
1907                    f"which was not within the expected range of {volt_range}"
1908                )
1909                failed_tests.append("HITL Terminal Voltage after temperature was set to 40°C")
1910            else:
1911                logger.write_result_to_html_report(
1912                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V,"
1913                    f"which was within the expected range of {volt_range}"
1914                )
1915
1916        if len(failed_tests) > 0:
1917            message = f"Overall, the following checks failed: {', '.join(failed_tests)}"
1918            logger.write_result_to_html_report(f'<font color="#990000">{message}</font>')
1919            pytest.fail(message)
1920
1921        logger.write_result_to_html_report("All checks passed test")
Description High Temperature Permanent Cutoff
GitHub Issue turnaroundfactor/HITL#516
Google Docs Google Sheet Cell
MIL-PRF Sections 3.7.2.5 (High Temperature permanent cut off devices)
Instructions 1. Set THERM1 and THERM2 to 23°C
2. Put cells in rested state at 4.2V per cell
3. Set THERM1 and THERM2 to 95°C
4. Measure Voltage
5. Attempt to charge at 1A
6. Attempt to discharge at 1A
7. Set temperature to 40°C
8. Measure Voltage
Pass / Fail Criteria ⦁ Expect Serial THERM1 & THERM 2 to be 95°C +/- 5°C
⦁ Expect Overtemperature Permanent Disable flag to be set
⦁ HITL Terminal Voltage is 0V +/- 0.2 V
⦁ HITL Terminal Current is 0A +/- 1mA after charging
⦁ HITL Terminal Current is 0A +/- 1mA after discharging
⦁ HITL Terminal Voltage is 0V +/- 0.2 V
Estimated Duration 6 seconds
Note The following test shall be performed. Charge batteries as specified in 4.6; use of 4.6.3 is permitted. Each battery shall be permanently shut off when the temperature of the battery reaches 199 ± 9°F (93 ± 5°C). The device shall prevent charging and discharging of the battery. The minimum quantity shall be as specified in the applicable specification sheet. When tested as specified in 4.7.4.10, battery voltage shall be zero volts after high temperature storage and shall remain at zero after 104°F (40°C) storage.
pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 4.2}]), kwargs={'indirect': True})]
@pytest.mark.parametrize('reset_test_environment', [{'volts': 4}], indirect=True)
class TestHighTemperatureTemporaryCutoff(hitl_tester.modules.bms.test_handler.CSVRecordEvent):
1924@pytest.mark.parametrize("reset_test_environment", [{"volts": 4}], indirect=True)
1925class TestHighTemperatureTemporaryCutoff(CSVRecordEvent):
1926    """Run a test for High temperature temporary cutoff"""
1927
1928    def test_high_temperature_temp_cutoff(self):
1929        """
1930        | Description          | High Temperature Temporary Cutoff                                      |
1931        | :------------------- | :--------------------------------------------------------------------- |
1932        | GitHub Issue         | turnaroundfactor/HITL#517                                       |
1933        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1934jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D27) |
1935        | MIL-PRF Sections     | 3.7.2.4 (High Temperature permanent cut off devices)                   |
1936        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
1937                                 2. Put cells in rested state at 4.2V per cell                     </br>\
1938                                 3. Set THERM1 and THERM2 to 75°C                                  </br>\
1939                                 4. Measure Voltage                                                </br>\
1940                                 5. Attempt to charge at 1A                                        </br>\
1941                                 6. Attempt to discharge at 1A                                     </br>\
1942                                 7. Set temperature to 20°C                                        </br>\
1943                                 8. Measure Voltage                                                     |
1944        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 75°C +/- 5°C               </br>\
1945                                 ⦁ Expect Overtemp charge & Overtemp discharge flags to be set     </br>\
1946                                 ⦁ HITL Terminal Voltage is 0V -0.2, +3.0 V                        </br>\
1947                                 ⦁ HITL Terminal Current is 0A +/- 1mA after charging            </br>\
1948                                 ⦁ HITL Terminal Current is 0A +/- 1mA after discharging         </br>\
1949                                 ⦁ HITL Terminal Voltage is 0V -0.2, +3.0 V                             |
1950        | Estimated Duration   | 12 seconds                                                             |
1951        | Note                 | Each battery shall contain a minimum quantity of normally closed       \
1952                                 thermoswitches that shall open at 158 ± 9°F (70 ± 5°C) and close at    \
1953                                 122 ± 9°F (50 ± 5°C). Each thermoswitch shall make physical contact    \
1954                                 with not less than one cell. The minimum quantity shall be as          \
1955                                 specified (see 3.1). The quantity of thermoswitches shall be           \
1956                                 certified. When tested as specified in 4.7.4.9, battery voltage shall  \
1957                                 be zero volts after each high temperature storage and batteries shall  \
1958                                 meet the voltage requirement of 3.5.2 after cooling. After completion  \
1959                                 of the test the battery shall be able to meet the full discharge       \
1960                                 capacity requirement of 3.5.3 after full charge.                       |
1961        """
1962
1963        failed_tests = []
1964        timeout_seconds = 30
1965
1966        temperature = 75
1967        high_range = temperature + 5
1968        low_range = temperature - 5
1969        logger.write_info_to_report(f"Setting THERM1 & THERM2 to {temperature}°C")
1970        _plateset.thermistor1 = _plateset.thermistor2 = temperature
1971
1972        time.sleep(1)
1973        serial_data = serial_monitor.read()  # Get the serial data
1974        # Convert temperature to Celsius from Kelvin
1975        therm_one = serial_data["dk_temp"] / 10 - 273
1976        therm_two = serial_data["dk_temp1"] / 10 - 273
1977        temp_range_text = f"{temperature}°C +/- 5°C"
1978
1979        if low_range <= therm_one <= high_range:
1980            logger.write_result_to_html_report(
1981                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range_text}"
1982            )
1983        else:
1984            logger.write_result_to_html_report(
1985                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
1986                f"of {temp_range_text}</font>"
1987            )
1988            failed_tests.append("THERM1")
1989
1990        if low_range <= therm_two <= high_range:
1991            logger.write_result_to_html_report(
1992                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range_text}"
1993            )
1994        else:
1995            logger.write_result_to_html_report(
1996                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
1997                f"expected range of {temp_range_text}</font>"
1998            )
1999            failed_tests.append("THERM2")
2000
2001        start = time.perf_counter()
2002        while (serial_data := serial_monitor.read()) and not serial_data["flags.fault_overtemp_discharge"]:
2003            if time.perf_counter() - start > timeout_seconds:
2004                message = f"Over-temperature discharge flag was not raised after {timeout_seconds} seconds."
2005                logger.write_failure_to_html_report(message)
2006                failed_tests.append("Over-temperature discharge flag.")
2007                break
2008        else:
2009            logger.write_result_to_html_report("Over-temperature discharge flag was properly set.")
2010
2011        logger.write_info_to_report("Measuring voltage...")
2012        time.sleep(1)
2013
2014        low_range = -0.2
2015        high_range = 3
2016        volt_range_text = f"0V ({low_range}V / +{high_range}V)"
2017
2018        with _bms.load(0.0, DischargeType.CONSTANT_RESISTANCE, 50_000):
2019            if not -0.2 <= _bms.dmm.volts <= 3:
2020                logger.write_failure_to_html_report(
2021                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2022                    f"which was not within the expected range of {volt_range_text}"
2023                )
2024                failed_tests.append("HITL Terminal Voltage")
2025            else:
2026                logger.write_result_to_html_report(
2027                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2028                    f"which was within the expected range of {volt_range_text}"
2029                )
2030
2031        logger.write_info_to_report("Attempting to charge at 1A")
2032        high_range = 0.020
2033        amp_range_text = "0A +/- 20mA"
2034        with _bms.charger(16.8, 1.0):
2035            time.sleep(1)
2036            if (high_range * -1) <= _bms.charger.amps <= high_range:
2037                logger.write_result_to_html_report(
2038                    f"HITL Terminal Current was {_bms.charger.amps:.3f}A when charging, which was within the "
2039                    f"expected range of {amp_range_text}"
2040                )
2041            else:
2042                logger.write_result_to_html_report(
2043                    f'<font color="#990000">HITL Terminal Current was {_bms.charger.amps:.3f}A when charging, '
2044                    f"which was not within the expected range of {amp_range_text} </font>"
2045                )
2046                failed_tests.append("HITL Terminal Current after attempting to charge at 1A")
2047
2048        logger.write_info_to_report("Attempting to discharge at 1A")
2049
2050        with _bms.load(1):
2051            time.sleep(1)
2052            if (high_range * -1) <= _bms.load.amps <= high_range:
2053                logger.write_result_to_html_report(
2054                    f"HITL Terminal Current was {_bms.load.amps:.3f}A when discharging, which was within the "
2055                    f"expected range of {amp_range_text}"
2056                )
2057            else:
2058                logger.write_result_to_html_report(
2059                    f'<font color="#990000">HITL Terminal Current was {_bms.load.amps:.3f}A when discharging, '
2060                    f"which was not within the expected range of {amp_range_text} </font>"
2061                )
2062                failed_tests.append("HITL Terminal Current after attempting to discharge at 1A")
2063
2064        temperature = 20
2065        logger.write_info_to_report(f"Setting Temperature to {temperature}°C")
2066        _plateset.thermistor1 = _plateset.thermistor2 = temperature
2067
2068        logger.write_info_to_report("Measuring voltage...")
2069        time.sleep(1)
2070        high_range = 17
2071        low_range = 9
2072        if not (_plateset.load_switch or _plateset.charger_switch):
2073            with _bms.load(0.0, DischargeType.CONSTANT_RESISTANCE, 50_000):
2074                if not low_range <= _bms.dmm.volts <= high_range:
2075                    logger.write_failure_to_html_report(
2076                        f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2077                        f"which was not within the expected range of {low_range}V to {high_range}V"
2078                    )
2079                    failed_tests.append("HITL Terminal Voltage")
2080                else:
2081                    logger.write_result_to_html_report(
2082                        f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2083                        f"which was within expected range of {low_range}V to {high_range}V"
2084                    )
2085        else:
2086            if not low_range <= _bms.dmm.volts <= high_range:
2087                logger.write_failure_to_html_report(
2088                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, which was not within the "
2089                    f"expected range of {low_range}V to {high_range}V"
2090                )
2091                failed_tests.append("HITL Terminal Voltage")
2092            else:
2093                logger.write_result_to_html_report(
2094                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2095                    f"which was within expected range of {low_range}V to {high_range}V"
2096                )
2097
2098        if len(failed_tests) > 0:
2099            pytest.fail()
2100
2101        logger.write_result_to_html_report("All checks passed test")

Run a test for High temperature temporary cutoff

def test_high_temperature_temp_cutoff(self):
1928    def test_high_temperature_temp_cutoff(self):
1929        """
1930        | Description          | High Temperature Temporary Cutoff                                      |
1931        | :------------------- | :--------------------------------------------------------------------- |
1932        | GitHub Issue         | turnaroundfactor/HITL#517                                       |
1933        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
1934jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D27) |
1935        | MIL-PRF Sections     | 3.7.2.4 (High Temperature permanent cut off devices)                   |
1936        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
1937                                 2. Put cells in rested state at 4.2V per cell                     </br>\
1938                                 3. Set THERM1 and THERM2 to 75°C                                  </br>\
1939                                 4. Measure Voltage                                                </br>\
1940                                 5. Attempt to charge at 1A                                        </br>\
1941                                 6. Attempt to discharge at 1A                                     </br>\
1942                                 7. Set temperature to 20°C                                        </br>\
1943                                 8. Measure Voltage                                                     |
1944        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 75°C +/- 5°C               </br>\
1945                                 ⦁ Expect Overtemp charge & Overtemp discharge flags to be set     </br>\
1946                                 ⦁ HITL Terminal Voltage is 0V -0.2, +3.0 V                        </br>\
1947                                 ⦁ HITL Terminal Current is 0A +/- 1mA after charging            </br>\
1948                                 ⦁ HITL Terminal Current is 0A +/- 1mA after discharging         </br>\
1949                                 ⦁ HITL Terminal Voltage is 0V -0.2, +3.0 V                             |
1950        | Estimated Duration   | 12 seconds                                                             |
1951        | Note                 | Each battery shall contain a minimum quantity of normally closed       \
1952                                 thermoswitches that shall open at 158 ± 9°F (70 ± 5°C) and close at    \
1953                                 122 ± 9°F (50 ± 5°C). Each thermoswitch shall make physical contact    \
1954                                 with not less than one cell. The minimum quantity shall be as          \
1955                                 specified (see 3.1). The quantity of thermoswitches shall be           \
1956                                 certified. When tested as specified in 4.7.4.9, battery voltage shall  \
1957                                 be zero volts after each high temperature storage and batteries shall  \
1958                                 meet the voltage requirement of 3.5.2 after cooling. After completion  \
1959                                 of the test the battery shall be able to meet the full discharge       \
1960                                 capacity requirement of 3.5.3 after full charge.                       |
1961        """
1962
1963        failed_tests = []
1964        timeout_seconds = 30
1965
1966        temperature = 75
1967        high_range = temperature + 5
1968        low_range = temperature - 5
1969        logger.write_info_to_report(f"Setting THERM1 & THERM2 to {temperature}°C")
1970        _plateset.thermistor1 = _plateset.thermistor2 = temperature
1971
1972        time.sleep(1)
1973        serial_data = serial_monitor.read()  # Get the serial data
1974        # Convert temperature to Celsius from Kelvin
1975        therm_one = serial_data["dk_temp"] / 10 - 273
1976        therm_two = serial_data["dk_temp1"] / 10 - 273
1977        temp_range_text = f"{temperature}°C +/- 5°C"
1978
1979        if low_range <= therm_one <= high_range:
1980            logger.write_result_to_html_report(
1981                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range_text}"
1982            )
1983        else:
1984            logger.write_result_to_html_report(
1985                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
1986                f"of {temp_range_text}</font>"
1987            )
1988            failed_tests.append("THERM1")
1989
1990        if low_range <= therm_two <= high_range:
1991            logger.write_result_to_html_report(
1992                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range_text}"
1993            )
1994        else:
1995            logger.write_result_to_html_report(
1996                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
1997                f"expected range of {temp_range_text}</font>"
1998            )
1999            failed_tests.append("THERM2")
2000
2001        start = time.perf_counter()
2002        while (serial_data := serial_monitor.read()) and not serial_data["flags.fault_overtemp_discharge"]:
2003            if time.perf_counter() - start > timeout_seconds:
2004                message = f"Over-temperature discharge flag was not raised after {timeout_seconds} seconds."
2005                logger.write_failure_to_html_report(message)
2006                failed_tests.append("Over-temperature discharge flag.")
2007                break
2008        else:
2009            logger.write_result_to_html_report("Over-temperature discharge flag was properly set.")
2010
2011        logger.write_info_to_report("Measuring voltage...")
2012        time.sleep(1)
2013
2014        low_range = -0.2
2015        high_range = 3
2016        volt_range_text = f"0V ({low_range}V / +{high_range}V)"
2017
2018        with _bms.load(0.0, DischargeType.CONSTANT_RESISTANCE, 50_000):
2019            if not -0.2 <= _bms.dmm.volts <= 3:
2020                logger.write_failure_to_html_report(
2021                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2022                    f"which was not within the expected range of {volt_range_text}"
2023                )
2024                failed_tests.append("HITL Terminal Voltage")
2025            else:
2026                logger.write_result_to_html_report(
2027                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2028                    f"which was within the expected range of {volt_range_text}"
2029                )
2030
2031        logger.write_info_to_report("Attempting to charge at 1A")
2032        high_range = 0.020
2033        amp_range_text = "0A +/- 20mA"
2034        with _bms.charger(16.8, 1.0):
2035            time.sleep(1)
2036            if (high_range * -1) <= _bms.charger.amps <= high_range:
2037                logger.write_result_to_html_report(
2038                    f"HITL Terminal Current was {_bms.charger.amps:.3f}A when charging, which was within the "
2039                    f"expected range of {amp_range_text}"
2040                )
2041            else:
2042                logger.write_result_to_html_report(
2043                    f'<font color="#990000">HITL Terminal Current was {_bms.charger.amps:.3f}A when charging, '
2044                    f"which was not within the expected range of {amp_range_text} </font>"
2045                )
2046                failed_tests.append("HITL Terminal Current after attempting to charge at 1A")
2047
2048        logger.write_info_to_report("Attempting to discharge at 1A")
2049
2050        with _bms.load(1):
2051            time.sleep(1)
2052            if (high_range * -1) <= _bms.load.amps <= high_range:
2053                logger.write_result_to_html_report(
2054                    f"HITL Terminal Current was {_bms.load.amps:.3f}A when discharging, which was within the "
2055                    f"expected range of {amp_range_text}"
2056                )
2057            else:
2058                logger.write_result_to_html_report(
2059                    f'<font color="#990000">HITL Terminal Current was {_bms.load.amps:.3f}A when discharging, '
2060                    f"which was not within the expected range of {amp_range_text} </font>"
2061                )
2062                failed_tests.append("HITL Terminal Current after attempting to discharge at 1A")
2063
2064        temperature = 20
2065        logger.write_info_to_report(f"Setting Temperature to {temperature}°C")
2066        _plateset.thermistor1 = _plateset.thermistor2 = temperature
2067
2068        logger.write_info_to_report("Measuring voltage...")
2069        time.sleep(1)
2070        high_range = 17
2071        low_range = 9
2072        if not (_plateset.load_switch or _plateset.charger_switch):
2073            with _bms.load(0.0, DischargeType.CONSTANT_RESISTANCE, 50_000):
2074                if not low_range <= _bms.dmm.volts <= high_range:
2075                    logger.write_failure_to_html_report(
2076                        f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2077                        f"which was not within the expected range of {low_range}V to {high_range}V"
2078                    )
2079                    failed_tests.append("HITL Terminal Voltage")
2080                else:
2081                    logger.write_result_to_html_report(
2082                        f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2083                        f"which was within expected range of {low_range}V to {high_range}V"
2084                    )
2085        else:
2086            if not low_range <= _bms.dmm.volts <= high_range:
2087                logger.write_failure_to_html_report(
2088                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, which was not within the "
2089                    f"expected range of {low_range}V to {high_range}V"
2090                )
2091                failed_tests.append("HITL Terminal Voltage")
2092            else:
2093                logger.write_result_to_html_report(
2094                    f"HITL Terminal Voltage was {_bms.dmm.volts:.3f}V, "
2095                    f"which was within expected range of {low_range}V to {high_range}V"
2096                )
2097
2098        if len(failed_tests) > 0:
2099            pytest.fail()
2100
2101        logger.write_result_to_html_report("All checks passed test")
Description High Temperature Temporary Cutoff
GitHub Issue turnaroundfactor/HITL#517
Google Docs Google Sheet Cell
MIL-PRF Sections 3.7.2.4 (High Temperature permanent cut off devices)
Instructions 1. Set THERM1 and THERM2 to 23°C
2. Put cells in rested state at 4.2V per cell
3. Set THERM1 and THERM2 to 75°C
4. Measure Voltage
5. Attempt to charge at 1A
6. Attempt to discharge at 1A
7. Set temperature to 20°C
8. Measure Voltage
Pass / Fail Criteria ⦁ Expect Serial THERM1 & THERM 2 to be 75°C +/- 5°C
⦁ Expect Overtemp charge & Overtemp discharge flags to be set
⦁ HITL Terminal Voltage is 0V -0.2, +3.0 V
⦁ HITL Terminal Current is 0A +/- 1mA after charging
⦁ HITL Terminal Current is 0A +/- 1mA after discharging
⦁ HITL Terminal Voltage is 0V -0.2, +3.0 V
Estimated Duration 12 seconds
Note Each battery shall contain a minimum quantity of normally closed thermoswitches that shall open at 158 ± 9°F (70 ± 5°C) and close at 122 ± 9°F (50 ± 5°C). Each thermoswitch shall make physical contact with not less than one cell. The minimum quantity shall be as specified (see 3.1). The quantity of thermoswitches shall be certified. When tested as specified in 4.7.4.9, battery voltage shall be zero volts after each high temperature storage and batteries shall meet the voltage requirement of 3.5.2 after cooling. After completion of the test the battery shall be able to meet the full discharge capacity requirement of 3.5.3 after full charge.
pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 4}]), kwargs={'indirect': True})]
@pytest.mark.parametrize('reset_test_environment', [{'volts': 4}], indirect=True)
class TestExtremeLowTempDischarge(hitl_tester.modules.bms.test_handler.CSVRecordEvent):
2104@pytest.mark.parametrize("reset_test_environment", [{"volts": 4}], indirect=True)
2105class TestExtremeLowTempDischarge(CSVRecordEvent):
2106    """Run a test for extreme low temperature discharge"""
2107
2108    def test_extreme_low_temp_discharge(self):
2109        """
2110        | Description          | Extreme low temperature discharge                                      |
2111        | :------------------- | :--------------------------------------------------------------------- |
2112        | GitHub Issue         | turnaroundfactor/HITL#518                                       |
2113        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2114jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D28) |
2115        | MIL-PRF Sections     | 4.7.3.2 (Extreme low temperature discharge)                            |
2116        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
2117                                 2. Put cells in rested state at 4.2V per cell                     </br>\
2118                                 3. Set THERM1 and THERM2 to -30°C                                 </br>\
2119                                 4. Attempt to discharge at 1A                                          |
2120        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be -30°C +/- 1.1°C            </br>\
2121                                 ⦁ Expect HITL Terminal Current to be -1A +/- 30mA                      |
2122        | Estimated Duration   | 6 seconds                                                              |
2123        | Note                 | For Type III (Li-ion) batteries the following test shall be performed. \
2124                                 Charge batteries in accordance with 4.6; use of 4.6.3 is not permitted.\
2125                                 Store the batteries at -22 ± 2°F (-30 ± 1.1°C) for a minimum of        \
2126                                 4 hours. Discharge under these conditions at the rate specified to the \
2127                                 specified cutoff voltage (see 3.1). After testing, batteries shall     \
2128                                 meet the requirements of 3.5.3, 3.6, and 3.6.1.                        |
2129        """
2130
2131        failed_tests = []
2132
2133        logger.write_info_to_report("Setting THERM1 & THERM2 to -30°C")
2134        _plateset.disengage_safety_protocols = True
2135        _plateset.thermistor1 = _plateset.thermistor2 = -30
2136        _plateset.disengage_safety_protocols = False
2137
2138        # Get the serial data
2139        serial_data = serial_monitor.read()
2140
2141        # Convert temperature to Celsius from Kelvin
2142        therm_one = serial_data["dk_temp"] / 10 - 273
2143        therm_two = serial_data["dk_temp1"] / 10 - 273
2144        temp_range = "-30°C +/- 1.1°C"
2145
2146        if -31.1 <= therm_one <= -28.9:
2147            logger.write_result_to_html_report(
2148                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
2149            )
2150        else:
2151            logger.write_result_to_html_report(
2152                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
2153                f"of {temp_range}</font>"
2154            )
2155            failed_tests.append("THERM1 after setting temperature to -30°C")
2156
2157        if -31.1 <= therm_two <= -28.9:
2158            logger.write_result_to_html_report(
2159                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
2160            )
2161        else:
2162            logger.write_result_to_html_report(
2163                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
2164                f"expected range of {temp_range}</font>"
2165            )
2166            failed_tests.append("THERM2 after setting temperature to -30°C")
2167
2168        logger.write_info_to_report("Attempting to discharge at 1A")
2169
2170        with _bms.load(1):
2171            time.sleep(1)
2172            load_amps = -1 * _bms.load.amps
2173            expected_current_range = "-1A +/- 30mA"
2174            if -1.03 <= load_amps <= -0.97:
2175                logger.write_result_to_html_report(
2176                    f"HITL Terminal Current was {load_amps:.3f}A, which was within the "
2177                    f"expected range of {expected_current_range}"
2178                )
2179            else:
2180                logger.write_result_to_html_report(
2181                    f'<font color="#990000">HITL Terminal Current was {load_amps:.3f}A, '
2182                    f"which was not within the expected range of {expected_current_range} </font>"
2183                )
2184                failed_tests.append("HITL Terminal Current after attempting to discharge at 1A")
2185
2186        if len(failed_tests) > 0:
2187            message = f"Overall, the following checks failed: {', '.join(failed_tests)}"
2188            logger.write_result_to_html_report(f'<font color="#990000">{message}</font>')
2189            pytest.fail(message)
2190
2191        logger.write_result_to_html_report("All checks passed test")

Run a test for extreme low temperature discharge

def test_extreme_low_temp_discharge(self):
2108    def test_extreme_low_temp_discharge(self):
2109        """
2110        | Description          | Extreme low temperature discharge                                      |
2111        | :------------------- | :--------------------------------------------------------------------- |
2112        | GitHub Issue         | turnaroundfactor/HITL#518                                       |
2113        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2114jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D28) |
2115        | MIL-PRF Sections     | 4.7.3.2 (Extreme low temperature discharge)                            |
2116        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
2117                                 2. Put cells in rested state at 4.2V per cell                     </br>\
2118                                 3. Set THERM1 and THERM2 to -30°C                                 </br>\
2119                                 4. Attempt to discharge at 1A                                          |
2120        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be -30°C +/- 1.1°C            </br>\
2121                                 ⦁ Expect HITL Terminal Current to be -1A +/- 30mA                      |
2122        | Estimated Duration   | 6 seconds                                                              |
2123        | Note                 | For Type III (Li-ion) batteries the following test shall be performed. \
2124                                 Charge batteries in accordance with 4.6; use of 4.6.3 is not permitted.\
2125                                 Store the batteries at -22 ± 2°F (-30 ± 1.1°C) for a minimum of        \
2126                                 4 hours. Discharge under these conditions at the rate specified to the \
2127                                 specified cutoff voltage (see 3.1). After testing, batteries shall     \
2128                                 meet the requirements of 3.5.3, 3.6, and 3.6.1.                        |
2129        """
2130
2131        failed_tests = []
2132
2133        logger.write_info_to_report("Setting THERM1 & THERM2 to -30°C")
2134        _plateset.disengage_safety_protocols = True
2135        _plateset.thermistor1 = _plateset.thermistor2 = -30
2136        _plateset.disengage_safety_protocols = False
2137
2138        # Get the serial data
2139        serial_data = serial_monitor.read()
2140
2141        # Convert temperature to Celsius from Kelvin
2142        therm_one = serial_data["dk_temp"] / 10 - 273
2143        therm_two = serial_data["dk_temp1"] / 10 - 273
2144        temp_range = "-30°C +/- 1.1°C"
2145
2146        if -31.1 <= therm_one <= -28.9:
2147            logger.write_result_to_html_report(
2148                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
2149            )
2150        else:
2151            logger.write_result_to_html_report(
2152                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
2153                f"of {temp_range}</font>"
2154            )
2155            failed_tests.append("THERM1 after setting temperature to -30°C")
2156
2157        if -31.1 <= therm_two <= -28.9:
2158            logger.write_result_to_html_report(
2159                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
2160            )
2161        else:
2162            logger.write_result_to_html_report(
2163                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
2164                f"expected range of {temp_range}</font>"
2165            )
2166            failed_tests.append("THERM2 after setting temperature to -30°C")
2167
2168        logger.write_info_to_report("Attempting to discharge at 1A")
2169
2170        with _bms.load(1):
2171            time.sleep(1)
2172            load_amps = -1 * _bms.load.amps
2173            expected_current_range = "-1A +/- 30mA"
2174            if -1.03 <= load_amps <= -0.97:
2175                logger.write_result_to_html_report(
2176                    f"HITL Terminal Current was {load_amps:.3f}A, which was within the "
2177                    f"expected range of {expected_current_range}"
2178                )
2179            else:
2180                logger.write_result_to_html_report(
2181                    f'<font color="#990000">HITL Terminal Current was {load_amps:.3f}A, '
2182                    f"which was not within the expected range of {expected_current_range} </font>"
2183                )
2184                failed_tests.append("HITL Terminal Current after attempting to discharge at 1A")
2185
2186        if len(failed_tests) > 0:
2187            message = f"Overall, the following checks failed: {', '.join(failed_tests)}"
2188            logger.write_result_to_html_report(f'<font color="#990000">{message}</font>')
2189            pytest.fail(message)
2190
2191        logger.write_result_to_html_report("All checks passed test")
Description Extreme low temperature discharge
GitHub Issue turnaroundfactor/HITL#518
Google Docs Google Sheet Cell
MIL-PRF Sections 4.7.3.2 (Extreme low temperature discharge)
Instructions 1. Set THERM1 and THERM2 to 23°C
2. Put cells in rested state at 4.2V per cell
3. Set THERM1 and THERM2 to -30°C
4. Attempt to discharge at 1A
Pass / Fail Criteria ⦁ Expect Serial THERM1 & THERM 2 to be -30°C +/- 1.1°C
⦁ Expect HITL Terminal Current to be -1A +/- 30mA
Estimated Duration 6 seconds
Note For Type III (Li-ion) batteries the following test shall be performed. Charge batteries in accordance with 4.6; use of 4.6.3 is not permitted. Store the batteries at -22 ± 2°F (-30 ± 1.1°C) for a minimum of 4 hours. Discharge under these conditions at the rate specified to the specified cutoff voltage (see 3.1). After testing, batteries shall meet the requirements of 3.5.3, 3.6, and 3.6.1.
pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 4}]), kwargs={'indirect': True})]
@pytest.mark.parametrize('reset_test_environment', [{'volts': 4}], indirect=True)
class TestExtremeHighTempDischarge(hitl_tester.modules.bms.test_handler.CSVRecordEvent):
2194@pytest.mark.parametrize("reset_test_environment", [{"volts": 4}], indirect=True)
2195class TestExtremeHighTempDischarge(CSVRecordEvent):
2196    """Run a test for extreme high temperature discharge"""
2197
2198    def test_extreme_high_temp_discharge(self):
2199        """
2200        | Description          | Extreme high temperature discharge                                     |
2201        | :------------------- | :--------------------------------------------------------------------- |
2202        | GitHub Issue         | turnaroundfactor/HITL#519                                       |
2203        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2204jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D31) |
2205        | MIL-PRF Sections     | 4.7.3.3 (Extreme high temperature discharge)                           |
2206        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
2207                                 2. Put cells in rested state at 4.2V per cell                     </br>\
2208                                 3. Set THERM1 and THERM2 to 55°C                                  </br>\
2209                                 4. Attempt to discharge at 1A                                          |
2210        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 55°C +/- 1.1°C             </br>\
2211                                 ⦁ Expect HITL Terminal Current to be -1A +/- 30mA                      |
2212        | Estimated Duration   | 6 seconds                                                              |
2213        | Note                 | For Type III (Li-ion) batteries the following test shall be performed. \
2214                                 Charge batteries in accordance with 4.6; use of 4.6.3 is not           \
2215                                 permitted. Store the batteries at 131 ± 2°F (55 ± 1.1°C) for a         \
2216                                 minimum of 4 hours. Discharge under these conditions at the rate       \
2217                                 specified to the specified cutoff voltage (see 3.1). After testing,    \
2218                                 batteries shall meet the requirements of 3.5.3, 3.6, and 3.6.1.        |
2219        """
2220
2221        failed_tests = []
2222        set_temp = 55
2223        logger.write_info_to_report(f"Setting THERM1 & THERM2 to {set_temp}°C")
2224        _plateset.disengage_safety_protocols = True
2225        _plateset.thermistor1 = _plateset.thermistor2 = set_temp
2226        _plateset.disengage_safety_protocols = False
2227
2228        # Get the serial data
2229        serial_data = serial_monitor.read()
2230
2231        # Convert temperature to Celsius from Kelvin
2232        therm_one = serial_data["dk_temp"] / 10 - 273
2233        therm_two = serial_data["dk_temp1"] / 10 - 273
2234        temp_range = f"{set_temp}°C +/- 1.1°C"
2235        low_range = set_temp - 1.1
2236        high_range = set_temp + 1.1
2237
2238        if low_range <= therm_one <= high_range:
2239            logger.write_result_to_html_report(
2240                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
2241            )
2242        else:
2243            logger.write_result_to_html_report(
2244                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
2245                f"of {temp_range}</font>"
2246            )
2247            failed_tests.append(f"THERM1 after setting temperature to {set_temp}°C")
2248
2249        if low_range <= therm_two <= high_range:
2250            logger.write_result_to_html_report(
2251                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
2252            )
2253        else:
2254            logger.write_result_to_html_report(
2255                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
2256                f"expected range of {temp_range}</font>"
2257            )
2258            failed_tests.append(f"THERM2 after setting temperature to {set_temp}°C")
2259
2260        logger.write_info_to_report("Attempting to discharge at 1A")
2261
2262        with _bms.load(1):
2263            time.sleep(1)
2264            load_amps = -1 * _bms.load.amps
2265            low_range = -1 - 0.03
2266            high_range = -1 + 0.03
2267            expected_current_range = "-1A +/- 30mA"
2268            if low_range <= load_amps <= high_range:
2269                logger.write_result_to_html_report(
2270                    f"HITL Terminal Current was {load_amps:.3f}A, which was within the "
2271                    f"expected range of {expected_current_range}"
2272                )
2273            else:
2274                logger.write_result_to_html_report(
2275                    f'<font color="#990000">HITL Terminal Current was {load_amps:.3f}A, '
2276                    f"which was not within the expected range of {expected_current_range} </font>"
2277                )
2278                failed_tests.append("HITL Terminal Current after attempting to discharge at 1A")
2279
2280        if len(failed_tests) > 0:
2281            message = f"Overall, the following checks failed: {', '.join(failed_tests)}"
2282            logger.write_result_to_html_report(f'<font color="#990000">{message}</font>')
2283            pytest.fail(message)
2284
2285        logger.write_result_to_html_report("All checks passed test")

Run a test for extreme high temperature discharge

def test_extreme_high_temp_discharge(self):
2198    def test_extreme_high_temp_discharge(self):
2199        """
2200        | Description          | Extreme high temperature discharge                                     |
2201        | :------------------- | :--------------------------------------------------------------------- |
2202        | GitHub Issue         | turnaroundfactor/HITL#519                                       |
2203        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2204jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D31) |
2205        | MIL-PRF Sections     | 4.7.3.3 (Extreme high temperature discharge)                           |
2206        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
2207                                 2. Put cells in rested state at 4.2V per cell                     </br>\
2208                                 3. Set THERM1 and THERM2 to 55°C                                  </br>\
2209                                 4. Attempt to discharge at 1A                                          |
2210        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 55°C +/- 1.1°C             </br>\
2211                                 ⦁ Expect HITL Terminal Current to be -1A +/- 30mA                      |
2212        | Estimated Duration   | 6 seconds                                                              |
2213        | Note                 | For Type III (Li-ion) batteries the following test shall be performed. \
2214                                 Charge batteries in accordance with 4.6; use of 4.6.3 is not           \
2215                                 permitted. Store the batteries at 131 ± 2°F (55 ± 1.1°C) for a         \
2216                                 minimum of 4 hours. Discharge under these conditions at the rate       \
2217                                 specified to the specified cutoff voltage (see 3.1). After testing,    \
2218                                 batteries shall meet the requirements of 3.5.3, 3.6, and 3.6.1.        |
2219        """
2220
2221        failed_tests = []
2222        set_temp = 55
2223        logger.write_info_to_report(f"Setting THERM1 & THERM2 to {set_temp}°C")
2224        _plateset.disengage_safety_protocols = True
2225        _plateset.thermistor1 = _plateset.thermistor2 = set_temp
2226        _plateset.disengage_safety_protocols = False
2227
2228        # Get the serial data
2229        serial_data = serial_monitor.read()
2230
2231        # Convert temperature to Celsius from Kelvin
2232        therm_one = serial_data["dk_temp"] / 10 - 273
2233        therm_two = serial_data["dk_temp1"] / 10 - 273
2234        temp_range = f"{set_temp}°C +/- 1.1°C"
2235        low_range = set_temp - 1.1
2236        high_range = set_temp + 1.1
2237
2238        if low_range <= therm_one <= high_range:
2239            logger.write_result_to_html_report(
2240                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
2241            )
2242        else:
2243            logger.write_result_to_html_report(
2244                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
2245                f"of {temp_range}</font>"
2246            )
2247            failed_tests.append(f"THERM1 after setting temperature to {set_temp}°C")
2248
2249        if low_range <= therm_two <= high_range:
2250            logger.write_result_to_html_report(
2251                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
2252            )
2253        else:
2254            logger.write_result_to_html_report(
2255                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
2256                f"expected range of {temp_range}</font>"
2257            )
2258            failed_tests.append(f"THERM2 after setting temperature to {set_temp}°C")
2259
2260        logger.write_info_to_report("Attempting to discharge at 1A")
2261
2262        with _bms.load(1):
2263            time.sleep(1)
2264            load_amps = -1 * _bms.load.amps
2265            low_range = -1 - 0.03
2266            high_range = -1 + 0.03
2267            expected_current_range = "-1A +/- 30mA"
2268            if low_range <= load_amps <= high_range:
2269                logger.write_result_to_html_report(
2270                    f"HITL Terminal Current was {load_amps:.3f}A, which was within the "
2271                    f"expected range of {expected_current_range}"
2272                )
2273            else:
2274                logger.write_result_to_html_report(
2275                    f'<font color="#990000">HITL Terminal Current was {load_amps:.3f}A, '
2276                    f"which was not within the expected range of {expected_current_range} </font>"
2277                )
2278                failed_tests.append("HITL Terminal Current after attempting to discharge at 1A")
2279
2280        if len(failed_tests) > 0:
2281            message = f"Overall, the following checks failed: {', '.join(failed_tests)}"
2282            logger.write_result_to_html_report(f'<font color="#990000">{message}</font>')
2283            pytest.fail(message)
2284
2285        logger.write_result_to_html_report("All checks passed test")
Description Extreme high temperature discharge
GitHub Issue turnaroundfactor/HITL#519
Google Docs Google Sheet Cell
MIL-PRF Sections 4.7.3.3 (Extreme high temperature discharge)
Instructions 1. Set THERM1 and THERM2 to 23°C
2. Put cells in rested state at 4.2V per cell
3. Set THERM1 and THERM2 to 55°C
4. Attempt to discharge at 1A
Pass / Fail Criteria ⦁ Expect Serial THERM1 & THERM 2 to be 55°C +/- 1.1°C
⦁ Expect HITL Terminal Current to be -1A +/- 30mA
Estimated Duration 6 seconds
Note For Type III (Li-ion) batteries the following test shall be performed. Charge batteries in accordance with 4.6; use of 4.6.3 is not permitted. Store the batteries at 131 ± 2°F (55 ± 1.1°C) for a minimum of 4 hours. Discharge under these conditions at the rate specified to the specified cutoff voltage (see 3.1). After testing, batteries shall meet the requirements of 3.5.3, 3.6, and 3.6.1.
pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 4}]), kwargs={'indirect': True})]
class TestStateTransitions:
2288class TestStateTransitions:
2289    """Run a test for state transition"""
2290
2291    def test_fast_sample(self, serial_watcher: SerialWatcher):
2292        """
2293        | Description          | Enter and exit fast sample state                                       |
2294        | :------------------- | :--------------------------------------------------------------------- |
2295        | GitHub Issue         | turnaroundfactor/HITL#478                                       |
2296        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2297jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D10)                          |
2298        | MIL-PRF Sections     | 3.5.7 (Power consumption)                                              |
2299        | Instructions         | 1. Ensure that we are in slow sample (the default state)          </br>\
2300                                 2. Transition to fast sample by charging 50mA+                    </br>\
2301                                 3. Maintain 50mA+ and ensure we are still in fast sample after 5 minutes </br>
2302                                 4. Transition to slow sample by discharging less than -50mA       </br>\
2303                                 5. Transition back to fast sample by charging 50mA+               </br>\
2304                                 6. Transition to deep slumber by charging/discharging in the range -50mA to 50mA </br>
2305                                 7. Transition back to fast sample by charging 50mA+                    |
2306        | Pass / Fail Criteria | ⦁ Fail if we are unable to transition to the desired state             |
2307        | Estimated Duration   | 5 minutes                                                              |
2308        | Note                 | The power consumption for all electronics within the battery shall     \
2309                                 be less than 350 micro-ampere average, per battery or independent      \
2310                                 section where applicable, at 68 ± 5°F (20 ± 2.8°C) (See 4.7.2.13).     |
2311        """
2312        logger.write_info_to_report("Testing Fast Sample")
2313        assert _bms.load and _bms.charger  # Make sure hardware exists
2314
2315        logger.write_result_to_html_report("Slow sample")
2316        serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2317
2318        logger.write_result_to_html_report("Slow sample -> Fast sample")
2319        with _bms.charger(16.8, 0.200):
2320            serial_watcher.assert_true("BMS_State", BMSState.FAST_SAMPLE)
2321            logger.write_result_to_html_report("Fast sample after 5 minutes")
2322            time.sleep(5.5 * 60)
2323            serial_watcher.assert_true("BMS_State", BMSState.FAST_SAMPLE)
2324
2325        logger.write_result_to_html_report("Fast sample -> Slow sample")
2326        with _bms.load(0.200):
2327            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2328
2329        logger.write_result_to_html_report("Slow sample -> Fast sample")
2330        with _bms.charger(16.8, 0.200):
2331            serial_watcher.assert_true("BMS_State", BMSState.FAST_SAMPLE)
2332
2333        logger.write_result_to_html_report("Fast sample -> Deep slumber")
2334        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=15 * 60)
2335
2336    def test_slow_sample(self, serial_watcher: SerialWatcher):
2337        """
2338        | Description          | Enter and exit slow sample state                                       |
2339        | :------------------- | :--------------------------------------------------------------------- |
2340        | GitHub Issue         | turnaroundfactor/HITL#478                                       |
2341        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2342jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D10)                          |
2343        | MIL-PRF Sections     | 3.5.7 (Power consumption)                                              |
2344        | Instructions         | 1. Ensure that we are in slow sample (the default state)          </br>\
2345                                 2. Transition to deep slumber by charging/discharging in the range -46mA to 20mA </br>\
2346                                 3. Transition back to slow sample by discharging less than -50mA  </br>\
2347                                 4. Test deep slumber timer (discharge)                            </br>\
2348                                 5. Charge/discharge in the range -46mA to 20mA for 2 minutes      </br>\
2349                                 6. Discharge less than -50mA for 2 minutes                        </br>\
2350                                 7. Transition to deep slumber by charging/discharging in the range -46mA to </br>\
2351                                 20mA, ensuring it takes 5 minutes                                 </br>\
2352                                 8. Test deep slumber timer (charge)                               </br>\
2353                                 9. Charge/discharge in the range -46mA to 20mA for 2 minutes      </br>\
2354                                 10. Charge in the range 20mA to 50mA for 2 minutes (keeps the comparator on <\br>\
2355                                 without entering fast sample)                                     </br>\
2356                                 11. Transition to deep slumber by charging/discharging in the range -46mA to </br>\
2357                                 20mA, ensuring it takes 5 minutes                                      |
2358        | Pass / Fail Criteria | ⦁ Fail if we are unable to transition to the desired state             |
2359        | Estimated Duration   | 5 minutes                                                              |
2360        | Note                 | The power consumption for all electronics within the battery shall     \
2361                                 be less than 350 micro-ampere average, per battery or independent      \
2362                                 section where applicable, at 68 ± 5°F (20 ± 2.8°C) (See 4.7.2.13).     |
2363        """
2364        logger.write_info_to_report("Testing Slow Sample")
2365        assert _bms.load and _bms.charger  # Make sure hardware exists
2366
2367        logger.write_result_to_html_report("Slow sample")
2368        serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2369
2370        logger.write_result_to_html_report("Slow sample -> Deep slumber")
2371        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2372
2373        logger.write_result_to_html_report("Deep slumber -> Slow sample")
2374        with _bms.load(0.200):
2375            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2376
2377        logger.write_result_to_html_report("Slow sample -> Deep slumber after 5 minutes (discharging)")
2378        serial_watcher.assert_measurements()
2379        time.sleep(2 * 60)
2380        with _bms.load(0.200):
2381            time.sleep(2 * 60)
2382        wait_start_time = time.perf_counter()
2383        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2384        if state_events := serial_watcher.events.get("BMS_State"):
2385            logger.write_result_to_html_report(f"Receive time: {state_events[-1].time - wait_start_time:.6f} seconds")
2386            assert state_events[-1].time - wait_start_time >= 5 * 60
2387
2388        logger.write_result_to_html_report("Deep slumber -> Slow sample")
2389        with _bms.load(0.200):
2390            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2391
2392        logger.write_result_to_html_report("Slow sample -> Deep slumber after 5 minutes (charging)")
2393        serial_watcher.assert_measurements()
2394        time.sleep(2 * 60)
2395        with _bms.charger(16.8, 0.040):
2396            time.sleep(2 * 60)
2397        wait_start_time = time.perf_counter()
2398        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2399        if state_events := serial_watcher.events.get("BMS_State"):
2400            logger.write_result_to_html_report(f"Receive time: {state_events[-1].time - wait_start_time:.6f} seconds")
2401            assert state_events[-1].time - wait_start_time >= 5 * 60
2402
2403        logger.write_result_to_html_report("Slow sample state test passed")
2404
2405    def test_deep_slumber(self, serial_watcher: SerialWatcher):
2406        """
2407        | Description          | Enter and exit slow deep slumber state                                 |
2408        | :------------------- | :--------------------------------------------------------------------- |
2409        | GitHub Issue         | turnaroundfactor/HITL#478                                       |
2410        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2411jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D10)                          |
2412        | MIL-PRF Sections     | 3.5.7 (Power consumption)                                              |
2413        | Instructions         | 1. Ensure that we are in slow sample (the default state)          </br>\
2414                                 2. Transition to deep slumber by charging/discharging in the range -46mA to 20mA </br>\
2415                                 3. Transition back to slow sample by discharging less than -50mA  </br>\
2416                                 4. Transition to deep slumber by charging/discharging in the range -46mA to 20mA </br>\
2417                                 5. Transition back to slow sample by charging in the range 20mA to 40mA          </br>\
2418                                 6. Transition to deep slumber by charging/discharging in the range -46mA to 20mA </br>\
2419                                 7. Transition back to slow sample by setting charge enable low while resting |
2420        | Pass / Fail Criteria | ⦁ Fail if we are unable to transition to the desired state             |
2421        | Estimated Duration   | 5 minutes                                                              |
2422        | Note                 | The power consumption for all electronics within the battery shall     \
2423                                 be less than 350 micro-ampere average, per battery or independent      \
2424                                 section where applicable, at 68 ± 5°F (20 ± 2.8°C) (See 4.7.2.13).     |
2425        """
2426        logger.write_info_to_report("Testing Deep slumber")
2427        assert _bms.load and _bms.charger  # Make sure hardware exists
2428
2429        logger.write_result_to_html_report("Slow sample")
2430        serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE, wait_time=5 * 60)
2431
2432        logger.write_result_to_html_report("Slow sample -> Deep slumber")
2433        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2434
2435        logger.write_result_to_html_report("Deep slumber -> Slow sample (discharging)")
2436        with _bms.load(0.200):
2437            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2438
2439        logger.write_result_to_html_report("Slow sample -> Deep slumber")
2440        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2441
2442        logger.write_result_to_html_report("Deep slumber -> Slow sample (charging)")
2443        with _bms.charger(16.8, 0.040):
2444            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2445
2446        logger.write_result_to_html_report("Slow sample -> Deep slumber")
2447        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2448
2449        logger.write_result_to_html_report("Deep slumber -> Slow sample (CE pin)")
2450        _plateset.ce_switch = True
2451        try:
2452            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2453        finally:  # Ensure ce pin is reset regardless of outcome
2454            _plateset.ce_switch = False
2455
2456        logger.write_result_to_html_report("Deep slumber state test passed")

Run a test for state transition

def test_fast_sample( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
2291    def test_fast_sample(self, serial_watcher: SerialWatcher):
2292        """
2293        | Description          | Enter and exit fast sample state                                       |
2294        | :------------------- | :--------------------------------------------------------------------- |
2295        | GitHub Issue         | turnaroundfactor/HITL#478                                       |
2296        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2297jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D10)                          |
2298        | MIL-PRF Sections     | 3.5.7 (Power consumption)                                              |
2299        | Instructions         | 1. Ensure that we are in slow sample (the default state)          </br>\
2300                                 2. Transition to fast sample by charging 50mA+                    </br>\
2301                                 3. Maintain 50mA+ and ensure we are still in fast sample after 5 minutes </br>
2302                                 4. Transition to slow sample by discharging less than -50mA       </br>\
2303                                 5. Transition back to fast sample by charging 50mA+               </br>\
2304                                 6. Transition to deep slumber by charging/discharging in the range -50mA to 50mA </br>
2305                                 7. Transition back to fast sample by charging 50mA+                    |
2306        | Pass / Fail Criteria | ⦁ Fail if we are unable to transition to the desired state             |
2307        | Estimated Duration   | 5 minutes                                                              |
2308        | Note                 | The power consumption for all electronics within the battery shall     \
2309                                 be less than 350 micro-ampere average, per battery or independent      \
2310                                 section where applicable, at 68 ± 5°F (20 ± 2.8°C) (See 4.7.2.13).     |
2311        """
2312        logger.write_info_to_report("Testing Fast Sample")
2313        assert _bms.load and _bms.charger  # Make sure hardware exists
2314
2315        logger.write_result_to_html_report("Slow sample")
2316        serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2317
2318        logger.write_result_to_html_report("Slow sample -> Fast sample")
2319        with _bms.charger(16.8, 0.200):
2320            serial_watcher.assert_true("BMS_State", BMSState.FAST_SAMPLE)
2321            logger.write_result_to_html_report("Fast sample after 5 minutes")
2322            time.sleep(5.5 * 60)
2323            serial_watcher.assert_true("BMS_State", BMSState.FAST_SAMPLE)
2324
2325        logger.write_result_to_html_report("Fast sample -> Slow sample")
2326        with _bms.load(0.200):
2327            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2328
2329        logger.write_result_to_html_report("Slow sample -> Fast sample")
2330        with _bms.charger(16.8, 0.200):
2331            serial_watcher.assert_true("BMS_State", BMSState.FAST_SAMPLE)
2332
2333        logger.write_result_to_html_report("Fast sample -> Deep slumber")
2334        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=15 * 60)
Description Enter and exit fast sample state
GitHub Issue turnaroundfactor/HITL#478
Google Docs Google Sheet Cell
MIL-PRF Sections 3.5.7 (Power consumption)
Instructions 1. Ensure that we are in slow sample (the default state)
2. Transition to fast sample by charging 50mA+
3. Maintain 50mA+ and ensure we are still in fast sample after 5 minutes

4. Transition to slow sample by discharging less than -50mA
5. Transition back to fast sample by charging 50mA+
6. Transition to deep slumber by charging/discharging in the range -50mA to 50mA
7. Transition back to fast sample by charging 50mA+ | | Pass / Fail Criteria | ⦁ Fail if we are unable to transition to the desired state | | Estimated Duration | 5 minutes | | Note | The power consumption for all electronics within the battery shall be less than 350 micro-ampere average, per battery or independent section where applicable, at 68 ± 5°F (20 ± 2.8°C) (See 4.7.2.13). |

def test_slow_sample( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
2336    def test_slow_sample(self, serial_watcher: SerialWatcher):
2337        """
2338        | Description          | Enter and exit slow sample state                                       |
2339        | :------------------- | :--------------------------------------------------------------------- |
2340        | GitHub Issue         | turnaroundfactor/HITL#478                                       |
2341        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2342jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D10)                          |
2343        | MIL-PRF Sections     | 3.5.7 (Power consumption)                                              |
2344        | Instructions         | 1. Ensure that we are in slow sample (the default state)          </br>\
2345                                 2. Transition to deep slumber by charging/discharging in the range -46mA to 20mA </br>\
2346                                 3. Transition back to slow sample by discharging less than -50mA  </br>\
2347                                 4. Test deep slumber timer (discharge)                            </br>\
2348                                 5. Charge/discharge in the range -46mA to 20mA for 2 minutes      </br>\
2349                                 6. Discharge less than -50mA for 2 minutes                        </br>\
2350                                 7. Transition to deep slumber by charging/discharging in the range -46mA to </br>\
2351                                 20mA, ensuring it takes 5 minutes                                 </br>\
2352                                 8. Test deep slumber timer (charge)                               </br>\
2353                                 9. Charge/discharge in the range -46mA to 20mA for 2 minutes      </br>\
2354                                 10. Charge in the range 20mA to 50mA for 2 minutes (keeps the comparator on <\br>\
2355                                 without entering fast sample)                                     </br>\
2356                                 11. Transition to deep slumber by charging/discharging in the range -46mA to </br>\
2357                                 20mA, ensuring it takes 5 minutes                                      |
2358        | Pass / Fail Criteria | ⦁ Fail if we are unable to transition to the desired state             |
2359        | Estimated Duration   | 5 minutes                                                              |
2360        | Note                 | The power consumption for all electronics within the battery shall     \
2361                                 be less than 350 micro-ampere average, per battery or independent      \
2362                                 section where applicable, at 68 ± 5°F (20 ± 2.8°C) (See 4.7.2.13).     |
2363        """
2364        logger.write_info_to_report("Testing Slow Sample")
2365        assert _bms.load and _bms.charger  # Make sure hardware exists
2366
2367        logger.write_result_to_html_report("Slow sample")
2368        serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2369
2370        logger.write_result_to_html_report("Slow sample -> Deep slumber")
2371        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2372
2373        logger.write_result_to_html_report("Deep slumber -> Slow sample")
2374        with _bms.load(0.200):
2375            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2376
2377        logger.write_result_to_html_report("Slow sample -> Deep slumber after 5 minutes (discharging)")
2378        serial_watcher.assert_measurements()
2379        time.sleep(2 * 60)
2380        with _bms.load(0.200):
2381            time.sleep(2 * 60)
2382        wait_start_time = time.perf_counter()
2383        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2384        if state_events := serial_watcher.events.get("BMS_State"):
2385            logger.write_result_to_html_report(f"Receive time: {state_events[-1].time - wait_start_time:.6f} seconds")
2386            assert state_events[-1].time - wait_start_time >= 5 * 60
2387
2388        logger.write_result_to_html_report("Deep slumber -> Slow sample")
2389        with _bms.load(0.200):
2390            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2391
2392        logger.write_result_to_html_report("Slow sample -> Deep slumber after 5 minutes (charging)")
2393        serial_watcher.assert_measurements()
2394        time.sleep(2 * 60)
2395        with _bms.charger(16.8, 0.040):
2396            time.sleep(2 * 60)
2397        wait_start_time = time.perf_counter()
2398        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2399        if state_events := serial_watcher.events.get("BMS_State"):
2400            logger.write_result_to_html_report(f"Receive time: {state_events[-1].time - wait_start_time:.6f} seconds")
2401            assert state_events[-1].time - wait_start_time >= 5 * 60
2402
2403        logger.write_result_to_html_report("Slow sample state test passed")
Description Enter and exit slow sample state
GitHub Issue turnaroundfactor/HITL#478
Google Docs Google Sheet Cell
MIL-PRF Sections 3.5.7 (Power consumption)
Instructions 1. Ensure that we are in slow sample (the default state)
2. Transition to deep slumber by charging/discharging in the range -46mA to 20mA
3. Transition back to slow sample by discharging less than -50mA
4. Test deep slumber timer (discharge)
5. Charge/discharge in the range -46mA to 20mA for 2 minutes
6. Discharge less than -50mA for 2 minutes
7. Transition to deep slumber by charging/discharging in the range -46mA to
20mA, ensuring it takes 5 minutes
8. Test deep slumber timer (charge)
9. Charge/discharge in the range -46mA to 20mA for 2 minutes
10. Charge in the range 20mA to 50mA for 2 minutes (keeps the comparator on <r> without entering fast sample)
11. Transition to deep slumber by charging/discharging in the range -46mA to
20mA, ensuring it takes 5 minutes
Pass / Fail Criteria ⦁ Fail if we are unable to transition to the desired state
Estimated Duration 5 minutes
Note The power consumption for all electronics within the battery shall be less than 350 micro-ampere average, per battery or independent section where applicable, at 68 ± 5°F (20 ± 2.8°C) (See 4.7.2.13).
def test_deep_slumber( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
2405    def test_deep_slumber(self, serial_watcher: SerialWatcher):
2406        """
2407        | Description          | Enter and exit slow deep slumber state                                 |
2408        | :------------------- | :--------------------------------------------------------------------- |
2409        | GitHub Issue         | turnaroundfactor/HITL#478                                       |
2410        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2411jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D10)                          |
2412        | MIL-PRF Sections     | 3.5.7 (Power consumption)                                              |
2413        | Instructions         | 1. Ensure that we are in slow sample (the default state)          </br>\
2414                                 2. Transition to deep slumber by charging/discharging in the range -46mA to 20mA </br>\
2415                                 3. Transition back to slow sample by discharging less than -50mA  </br>\
2416                                 4. Transition to deep slumber by charging/discharging in the range -46mA to 20mA </br>\
2417                                 5. Transition back to slow sample by charging in the range 20mA to 40mA          </br>\
2418                                 6. Transition to deep slumber by charging/discharging in the range -46mA to 20mA </br>\
2419                                 7. Transition back to slow sample by setting charge enable low while resting |
2420        | Pass / Fail Criteria | ⦁ Fail if we are unable to transition to the desired state             |
2421        | Estimated Duration   | 5 minutes                                                              |
2422        | Note                 | The power consumption for all electronics within the battery shall     \
2423                                 be less than 350 micro-ampere average, per battery or independent      \
2424                                 section where applicable, at 68 ± 5°F (20 ± 2.8°C) (See 4.7.2.13).     |
2425        """
2426        logger.write_info_to_report("Testing Deep slumber")
2427        assert _bms.load and _bms.charger  # Make sure hardware exists
2428
2429        logger.write_result_to_html_report("Slow sample")
2430        serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE, wait_time=5 * 60)
2431
2432        logger.write_result_to_html_report("Slow sample -> Deep slumber")
2433        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2434
2435        logger.write_result_to_html_report("Deep slumber -> Slow sample (discharging)")
2436        with _bms.load(0.200):
2437            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2438
2439        logger.write_result_to_html_report("Slow sample -> Deep slumber")
2440        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2441
2442        logger.write_result_to_html_report("Deep slumber -> Slow sample (charging)")
2443        with _bms.charger(16.8, 0.040):
2444            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2445
2446        logger.write_result_to_html_report("Slow sample -> Deep slumber")
2447        serial_watcher.assert_true("BMS_State", BMSState.DEEP_SLUMBER, wait_time=10 * 60)
2448
2449        logger.write_result_to_html_report("Deep slumber -> Slow sample (CE pin)")
2450        _plateset.ce_switch = True
2451        try:
2452            serial_watcher.assert_true("BMS_State", BMSState.SLOW_SAMPLE)
2453        finally:  # Ensure ce pin is reset regardless of outcome
2454            _plateset.ce_switch = False
2455
2456        logger.write_result_to_html_report("Deep slumber state test passed")
Description Enter and exit slow deep slumber state
GitHub Issue turnaroundfactor/HITL#478
Google Docs Google Sheet Cell
MIL-PRF Sections 3.5.7 (Power consumption)
Instructions 1. Ensure that we are in slow sample (the default state)
2. Transition to deep slumber by charging/discharging in the range -46mA to 20mA
3. Transition back to slow sample by discharging less than -50mA
4. Transition to deep slumber by charging/discharging in the range -46mA to 20mA
5. Transition back to slow sample by charging in the range 20mA to 40mA
6. Transition to deep slumber by charging/discharging in the range -46mA to 20mA
7. Transition back to slow sample by setting charge enable low while resting
Pass / Fail Criteria ⦁ Fail if we are unable to transition to the desired state
Estimated Duration 5 minutes
Note The power consumption for all electronics within the battery shall be less than 350 micro-ampere average, per battery or independent section where applicable, at 68 ± 5°F (20 ± 2.8°C) (See 4.7.2.13).
@pytest.mark.parametrize('reset_test_environment', [{'volts': 2.5}], indirect=True)
class TestVoltageAccuracy:
2459@pytest.mark.parametrize("reset_test_environment", [{"volts": 2.5}], indirect=True)
2460class TestVoltageAccuracy:
2461    """Run a test for voltage accuracy"""
2462
2463    class VoltageCellAccuracy(CSVRecordEvent):
2464        """@private Check if HITL cell sim voltage matches serial cell sim voltage within 1%"""
2465
2466        percent_closeness = 0.05
2467        max = SimpleNamespace(cell_id=0, sim_v=0, bms_v=0, error=0.0)
2468
2469        @classmethod
2470        def failed(cls) -> bool:
2471            """Check if test parameters were exceeded."""
2472            return bool(cls.max.error > cls.percent_closeness)
2473
2474        @classmethod
2475        def verify(cls, row, serial_data, _cell_data):
2476            """Cell voltage within range"""
2477            for i, cell_id in enumerate(_bms.cells):
2478                row_data = SimpleNamespace(
2479                    cell_id=cell_id,
2480                    sim_v=row[f"ADC Plate Cell {cell_id} Voltage (V)"],
2481                    bms_v=serial_data[f"mvolt_cell{'' if i == 0 else i}"] / 1000,
2482                )
2483                row_data.error = abs((row_data.bms_v - row_data.sim_v) / row_data.sim_v)
2484                cls.max = max(cls.max, row_data, key=lambda data: data.error)
2485
2486        @classmethod
2487        def result(cls):
2488            """Detailed test result information."""
2489            return (
2490                f"Cell Voltage Error: {cls.cmp(cls.max.error, '<=', cls.percent_closeness)}"
2491                f"(Sim {cls.max.cell_id}: {cls.max.sim_v * 1000:.1f} mv, BMS: {cls.max.bms_v * 1000:.1f} mv)"
2492            )
2493
2494    class TerminalVoltageAccuracy(CSVRecordEvent):
2495        """@private Compare HITL voltage to reported Terminal voltage."""
2496
2497        percent_closeness = 0.05
2498        max = SimpleNamespace(hitl_v=0, bms_v=0, error=0.0)
2499
2500        @classmethod
2501        def failed(cls) -> bool:
2502            """Check if test parameters were exceeded."""
2503            return bool(cls.max.error > cls.percent_closeness)
2504
2505        @classmethod
2506        def verify(cls, row, serial_data, _cell_data):
2507            """Terminal voltage within range"""
2508            row_data = SimpleNamespace(hitl_v=row["HITL Voltage (V)"], bms_v=serial_data["mvolt_terminal"] / 1000)
2509            row_data.error = abs((row_data.bms_v - row_data.hitl_v) / row_data.hitl_v)
2510            cls.max = max(cls.max, row_data, key=lambda data: data.error)
2511
2512        @classmethod
2513        def result(cls):
2514            """Detailed test result information."""
2515            return (
2516                f"Terminal Voltage error: {cls.cmp(cls.max.error, '<=', cls.percent_closeness)} "
2517                f"(HITL: {cls.max.hitl_v * 1000:.1f} mv, BMS: {cls.max.bms_v * 1000:.1f} mv)"
2518            )
2519
2520    def test_voltage_accuracy(self):
2521        """
2522        | Description          | Test the cell voltage accuracy                                         |
2523        | :------------------- | :--------------------------------------------------------------------- |
2524        | GitHub Issue         | turnaroundfactor/HITL#399                                       |
2525        | MIL-PRF Sections     | 3.5.8.3 (Accuracy)                                                </br>\
2526                                 4.7.2.14.3 (Accuracy During Discharge)                                 |
2527        | Instructions         | 1. Set thermistors to 23C                                         </br>\
2528                                 2. Put cells in a rested state at 2.5V per cell                   </br>\
2529                                 3. Charge battery (16.8V / 3A / 100 mA cutoff)                    </br>\
2530                                 4. Increase cell voltages in 100 mV increments up to and including 4.2V </br>\
2531        | Pass / Fail Criteria | ⦁ HITL cell sim voltage matches serial cell sim voltage to within 1%  </br>\
2532                                 ⦁ HITL voltage matches serial Terminal voltage to within 1%   </br>\
2533        | Estimated Duration   | 12 hours                                                               |
2534        | Note                 | MIL-PRF 3.5.8.3 (Accuracy): The values of the display shall be         \
2535                                 accurate within +0/-5% of the actual values for the battery.           \
2536                                 (see 4.7.2.14.3).                                                      |
2537        """
2538        _bms.timer.reset()  # Keep track of runtime
2539        for target_mv in range(2500, 4300, 100):
2540            voltages = ", ".join(f"{cell.volts}V" for cell in _bms.cells.values())
2541            logger.write_info_to_report(f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Voltage: {voltages}, ")
2542            _bms.csv.cycle.record(_bms.timer.elapsed_time)
2543            for cell in _bms.cells.values():
2544                cell.volts = target_mv / 1000
2545            time.sleep(3)
2546
2547        if CSVRecordEvent.failed():  # FIXME(JA): make this implicit?
2548            pytest.fail(CSVRecordEvent.result())

Run a test for voltage accuracy

def test_voltage_accuracy(self):
2520    def test_voltage_accuracy(self):
2521        """
2522        | Description          | Test the cell voltage accuracy                                         |
2523        | :------------------- | :--------------------------------------------------------------------- |
2524        | GitHub Issue         | turnaroundfactor/HITL#399                                       |
2525        | MIL-PRF Sections     | 3.5.8.3 (Accuracy)                                                </br>\
2526                                 4.7.2.14.3 (Accuracy During Discharge)                                 |
2527        | Instructions         | 1. Set thermistors to 23C                                         </br>\
2528                                 2. Put cells in a rested state at 2.5V per cell                   </br>\
2529                                 3. Charge battery (16.8V / 3A / 100 mA cutoff)                    </br>\
2530                                 4. Increase cell voltages in 100 mV increments up to and including 4.2V </br>\
2531        | Pass / Fail Criteria | ⦁ HITL cell sim voltage matches serial cell sim voltage to within 1%  </br>\
2532                                 ⦁ HITL voltage matches serial Terminal voltage to within 1%   </br>\
2533        | Estimated Duration   | 12 hours                                                               |
2534        | Note                 | MIL-PRF 3.5.8.3 (Accuracy): The values of the display shall be         \
2535                                 accurate within +0/-5% of the actual values for the battery.           \
2536                                 (see 4.7.2.14.3).                                                      |
2537        """
2538        _bms.timer.reset()  # Keep track of runtime
2539        for target_mv in range(2500, 4300, 100):
2540            voltages = ", ".join(f"{cell.volts}V" for cell in _bms.cells.values())
2541            logger.write_info_to_report(f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Voltage: {voltages}, ")
2542            _bms.csv.cycle.record(_bms.timer.elapsed_time)
2543            for cell in _bms.cells.values():
2544                cell.volts = target_mv / 1000
2545            time.sleep(3)
2546
2547        if CSVRecordEvent.failed():  # FIXME(JA): make this implicit?
2548            pytest.fail(CSVRecordEvent.result())
Description Test the cell voltage accuracy
GitHub Issue turnaroundfactor/HITL#399
MIL-PRF Sections 3.5.8.3 (Accuracy)
4.7.2.14.3 (Accuracy During Discharge)
Instructions 1. Set thermistors to 23C
2. Put cells in a rested state at 2.5V per cell
3. Charge battery (16.8V / 3A / 100 mA cutoff)
4. Increase cell voltages in 100 mV increments up to and including 4.2V
Pass / Fail Criteria ⦁ HITL cell sim voltage matches serial cell sim voltage to within 1%
⦁ HITL voltage matches serial Terminal voltage to within 1%
Estimated Duration 12 hours
Note MIL-PRF 3.5.8.3 (Accuracy): The values of the display shall be accurate within +0/-5% of the actual values for the battery. (see 4.7.2.14.3).
pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 2.5}]), kwargs={'indirect': True})]
@pytest.mark.parametrize('reset_test_environment', [{'volts': 3.5}], indirect=True)
class TestSerialFaults:
2552@pytest.mark.parametrize("reset_test_environment", [{"volts": 3.5}], indirect=True)
2553class TestSerialFaults:
2554    """Test all faults"""
2555
2556    def test_wakeup_1(self, serial_watcher: SerialWatcher):
2557        """
2558        | Description          | Test Wakeup 1                                                          |
2559        | :------------------- | :--------------------------------------------------------------------- |
2560        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2561        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2562jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2563        | MIL-PRF Sections     | 3.6.8.2 (Battery wake up)                                              |
2564        | Instructions         | 1. Set Default state at 0mA                                       </br>\
2565                                 2. Discharge at 100 mA (below -46 mA)                             </br>\
2566                                 3. Discharge at 10 mA (between -46mA and 19ma)                    </br>\
2567                                 4. Charge at 100 mA, (above 19 mA)                                     |
2568        | Pass / Fail Criteria | ⦁ n_wakeup_gpio = 1, with default state at 0 mA                   </br>\
2569                                 ⦁ n_wakeup_gpio = 0, with default state at 100 mA                   </br>\
2570                                 ⦁ n_wakeup_gpio = 1, with default state at 10 mA                   </br>\
2571                                 ⦁ n_wakeup_gpio = 0, with default state at 0 mA                        |
2572        | Estimated Duration   | 10 seconds                                                             |
2573        | Note                 | The BMS should be in a state of slumber if the current measured is     \
2574                                 between -46mA and 19ma. This is done using internal comparators on the \
2575                                 board to a logical AND chip feeding into an interrupt pin. To test     \
2576                                 this, 3 different currents should be set. One current below -46ma,     \
2577                                 another current between -46mA and 19ma, and another current above      \
2578                                 19ma. If the current is within the allowable range, we should read     \
2579                                 logic 1 on the N_WAKEUP pin. If the current is outside (above or       \
2580                                 below) we should read logic 0 on the pin.                              |
2581        """
2582
2583        logger.write_info_to_report("Testing Wakeup")
2584        assert _bms.load and _bms.charger  # Confirm hardware is available
2585
2586        logger.write_result_to_html_report("Default state")
2587        serial_watcher.assert_true("n_wakeup_gpio", True, 1)
2588
2589        logger.write_result_to_html_report("Discharging 200 mA")
2590        with _bms.load(0.200):
2591            serial_watcher.assert_true("n_wakeup_gpio", False, 2)
2592
2593        logger.write_result_to_html_report("Discharging 10 mA")
2594        with _bms.load(0.010):
2595            serial_watcher.assert_true("n_wakeup_gpio", True, 3)
2596
2597        logger.write_result_to_html_report("Charging 200 mA")
2598        with _bms.charger(16.8, 0.200):
2599            serial_watcher.assert_true("n_wakeup_gpio", False, 4)
2600
2601    def test_overtemp_fault(self, serial_watcher: SerialWatcher):
2602        """
2603        | Description          | Test over temperature Fault                                             |
2604        | :------------------- | :--------------------------------------------------------------------- |
2605        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2606        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2607jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2608        | MIL-PRF Sections     | 4.7.3.3 (Extreme high temperature discharge)                           |
2609        | Instructions         | 1. Set Default state at 15                                        </br>\
2610                                 2. Resting at 59°C (at or above 59°C)                             </br>\
2611                                 3. Discharging at 100 mA, 59°C (at or above 59°C)                 </br>\
2612                                 4. Charging at 100 mA, 54°C (at or above 53°C)                    </br>\
2613                                 5. Charging at 100 mA, 94°C (at or above 93°C)                    </br>\
2614                                 6. Discharging at 100 mA, 94°C (at or above 93°C)                 </br>\
2615                                 7. Rest at 94°C (at or above 93°C)                                      |
2616        | Pass / Fail Criteria | ⦁ Prefault overtemp Discharge value is False (default state)       </br>\
2617                                 ⦁ Fault overtemp Discharge value is False (default state)          </br>\
2618                                 ⦁ Prefault overtemp charge value is False (default state)          </br>\
2619                                 ⦁ Fault overtemp charge value is False (default state)             </br>\
2620                                 ⦁ Permanent Disable overtemp is False (default state)              </br>\
2621                                 ⦁ Measure output fets disabled is  False (default state)           </br>\
2622                                 ⦁ Resting overtemp discharge is True for fault & prefault          </br>\
2623                                 ⦁ Discharging overtemp value is True for fault & prefault          </br>\
2624                                 ⦁ After charging permanent disable, permanent disable overtemp is True </br>\
2625                                 ⦁ After resting permanent disable, permanent disable overtemp is True  |
2626        | Estimated Duration   | 12 hours                                                               |
2627        | Note                 | If our batteries get too hot, we must trigger a fault. This is         \
2628                                 common during high discharge or charging cycles. There are 3 different \
2629                                 environments where we would trigger an overtemp fault: charge,         \
2630                                 discharge and resting. While charging, if we are above 53C we must     \
2631                                 trigger a fault If we are resting or discharging at 59 degrees we must \
2632                                 trigger a fault. Both of these faults should trigger a prefault        \
2633                                 condition in our flags. After we cycle again we should then trigger a  \
2634                                 full fault. If the temperature ever goes above 93 degrees, the fault   \
2635                                 should never clear and we should be in permanent fault and trigger     \
2636                                 the fets. (This should also be seen in the flags)                      |
2637        """
2638        logger.write_info_to_report("Testing Overtemp")
2639        assert _bms.load and _bms.charger  # Confirm hardware is available
2640
2641        # Test default state
2642        logger.write_result_to_html_report(f"Resting at 0 mA, {CELL_VOLTAGE} V, 15°C")
2643        _plateset.thermistor1 = 15
2644        serial_watcher.assert_true("flags.prefault_overtemp_discharge", False, 1)
2645        serial_watcher.assert_true("flags.fault_overtemp_discharge", False, 1)
2646        serial_watcher.assert_true("flags.prefault_overtemp_charge", False, 1)
2647        serial_watcher.assert_true("flags.fault_overtemp_charge", False, 1)
2648        serial_watcher.assert_true("flags.permanentdisable_overtemp", False, 1)
2649        serial_watcher.assert_true("flags.measure_output_fets_disabled", False, 1)
2650
2651        # Test resting overtemp
2652        logger.write_result_to_html_report("Resting at 59°C")
2653        _plateset.thermistor1 = 59
2654        serial_watcher.assert_true("flags.prefault_overtemp_discharge", True, 2)
2655        serial_watcher.assert_true("flags.fault_overtemp_discharge", True, 2)
2656        logger.write_result_to_html_report("Resting at 15°C")
2657        _plateset.thermistor1 = 15
2658        serial_watcher.assert_true("flags.prefault_overtemp_discharge", False, 3)
2659        serial_watcher.assert_true("flags.fault_overtemp_discharge", False, 3)
2660
2661        # Test discharging overtemp
2662        logger.write_result_to_html_report("Discharging at -100 mA, 59°C")
2663        with _bms.load(0.100):
2664            _plateset.thermistor1 = 59
2665            serial_watcher.assert_true("flags.prefault_overtemp_discharge", True, 4)
2666            serial_watcher.assert_true("flags.fault_overtemp_discharge", True, 4)
2667            logger.write_result_to_html_report("Discharging at -100 mA, 15°C")
2668            _plateset.thermistor1 = 15
2669            serial_watcher.assert_true("flags.prefault_overtemp_discharge", False, 5)
2670            serial_watcher.assert_true("flags.fault_overtemp_discharge", False, 5)
2671
2672        # Test charging overtemp
2673        logger.write_result_to_html_report("Charging at 100 mA, 54°C")
2674        with _bms.charger(16.8, 0.200):
2675            _plateset.thermistor1 = 54
2676            serial_watcher.assert_true("flags.prefault_overtemp_charge", True, 2)
2677            serial_watcher.assert_true("flags.fault_overtemp_charge", True, 2)
2678            logger.write_result_to_html_report("Charging at 100 mA, 15°C")
2679            _plateset.thermistor1 = 15
2680            serial_watcher.assert_true("flags.prefault_overtemp_charge", False, 3)
2681            serial_watcher.assert_true("flags.fault_overtemp_charge", False, 3)
2682
2683        # Test charging permanent disable
2684        logger.write_result_to_html_report("Charging at 100 mA, 94°C")
2685        _plateset.disengage_safety_protocols = True
2686        _plateset.thermistor1 = 94
2687        _plateset.disengage_safety_protocols = False
2688        with _bms.charger(16.8, 0.200):
2689            serial_watcher.assert_true("flags.permanentdisable_overtemp", True, 2)
2690            logger.write_result_to_html_report("Charging at 100 mA, 15°C")
2691            _plateset.thermistor1 = 15
2692            serial_watcher.assert_false("flags.permanentdisable_overtemp", False, 3)
2693
2694        # Test discharging permanent disable
2695        logger.write_result_to_html_report("Discharging at -100 mA, 94°C")
2696        _plateset.disengage_safety_protocols = True
2697        _plateset.thermistor1 = 94
2698        _plateset.disengage_safety_protocols = False
2699        with _bms.load(0.100):
2700            serial_watcher.assert_true("flags.permanentdisable_overtemp", True, 2)
2701            logger.write_result_to_html_report("Discharging at -100 mA, 15°C")
2702            _plateset.thermistor1 = 15
2703            serial_watcher.assert_false("flags.permanentdisable_overtemp", False, 3)
2704
2705        # Test resting permanent disable
2706        _plateset.disengage_safety_protocols = True
2707        logger.write_result_to_html_report("Resting at 94°C")
2708        _plateset.thermistor1 = 94
2709        _plateset.disengage_safety_protocols = False
2710        serial_watcher.assert_true("flags.permanentdisable_overtemp", True, 2)
2711        logger.write_result_to_html_report("Resting at 15°C")
2712        _plateset.thermistor1 = 15
2713        serial_watcher.assert_false("flags.permanentdisable_overtemp", False, 3)
2714
2715    def test_undertemp_faults(self, serial_watcher: SerialWatcher):
2716        """
2717        | Description          | Test under temperature Fault                                             |
2718        | :------------------- | :--------------------------------------------------------------------- |
2719        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2720        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2721jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2722        | MIL-PRF Sections     | 4.7.2.7 (Low Temperature Discharge)                                    |
2723        | Instructions         | 1. Set Default state at 15                                        </br>\
2724                                 2. Resting at -35°C (at or below -33.2°C)                         </br>\
2725                                 3. Discharging at -100 mA, -35°C (at or below -33.2°C)            </br>\
2726                                 4. Discharging at -100 mA, -30°C (at or above -32.2°C)            </br>\
2727                                 5. Charging at 100 mA, -25°C (at or below -23.2°C)                </br>\
2728                                 6. Charging at 100 mA, -20°C (at or above -22.2°C)                     |
2729        | Pass / Fail Criteria | ⦁ Prefault overtemp Discharge value is False (default state)       </br>\
2730                                 ⦁ Fault overtemp Discharge value is False (default state)          </br>\
2731                                 ⦁ Prefault overtemp charge value is False (default state)          </br>\
2732                                 ⦁ Fault overtemp charge value is False (default state)             </br>\
2733                                 ⦁ Permanent Disable overtemp is False (default state)              </br>\
2734                                 ⦁ Measure output fets disabled is  False (default state)           </br>\
2735                                 ⦁ Resting overtemp discharge is True for fault & prefault          </br>\
2736                                 ⦁ Discharging overtemp value is True for fault & prefault          </br>\
2737                                 ⦁ After charging permanent disable, permanent disable overtemp is True </br>\
2738                                 ⦁ After resting permanent disable, permanent disable overtemp is True  |
2739        | Estimated Duration   | 10 seconds                                                             |
2740        | Note                 | This occurs when we read more than 20 mamps from the battery, if any   \
2741                                 of the cells are under 0 degrees Celsius this will trigger a fault.    \
2742                                 This will be cleared if we go above -2C. Regardless of current being   \
2743                                 measured, if we ever read below -20C, this should trigger a fault.     \
2744                                 This fault should not be cleared until we are above -18C               |
2745        """
2746        logger.write_info_to_report("Testing Undertemp")
2747        assert _bms.load and _bms.charger  # Confirm hardware is available
2748
2749        logger.write_result_to_html_report(f"Resting at 0 mA, {CELL_VOLTAGE} V, 15°C")
2750        _plateset.thermistor1 = 15
2751
2752        # Discharging flags
2753        serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 1)
2754        serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 1)
2755
2756        # Charging flags
2757        serial_watcher.assert_true("flags.prefault_undertemp_charge", False, 1)
2758        serial_watcher.assert_true("flags.fault_undertemp_charge", False, 1)
2759
2760        logger.write_result_to_html_report("Resting at -35°C")
2761        _plateset.thermistor1 = -35
2762        serial_watcher.assert_true("flags.prefault_undertemp_discharge", True, 2)
2763        serial_watcher.assert_true("flags.fault_undertemp_discharge", True, 2)
2764        logger.write_result_to_html_report("Resting at -30°C")
2765        _plateset.thermistor1 = -30
2766        serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 3)
2767        serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 3)
2768
2769        logger.write_result_to_html_report("Discharging at -100 mA, -35°C")
2770        with _bms.load(0.100):
2771            _plateset.thermistor1 = -35
2772            serial_watcher.assert_true("flags.prefault_undertemp_discharge", True, 4)
2773            serial_watcher.assert_true("flags.fault_undertemp_discharge", True, 4)
2774            logger.write_result_to_html_report("Discharging at -100 mA, -30°C")
2775            _plateset.thermistor1 = -30
2776            serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 5)
2777            serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 5)
2778
2779        logger.write_result_to_html_report("Charging at 100 mA, -25°C")
2780        with _bms.charger(16.8, 0.200):
2781            _plateset.thermistor1 = -25
2782            serial_watcher.assert_true("flags.prefault_undertemp_charge", True, 2)
2783            serial_watcher.assert_true("flags.fault_undertemp_charge", True, 2)
2784            logger.write_result_to_html_report("Charging at 100 mA, 20°C")
2785            _plateset.thermistor1 = 20
2786            serial_watcher.assert_true("flags.prefault_undertemp_charge", False, 3)
2787            serial_watcher.assert_true("flags.fault_undertemp_charge", False, 3)
2788
2789    def set_exact_volts(self, cell: Cell, voltage: float, compensation: float = 0.08):
2790        """What the BMS reads won't exactly match the set voltage, thus we need slight adjustments."""
2791        cell.exact_volts = voltage + compensation
2792        logger.write_debug_to_report(f"Cell is {cell.volts}V")
2793
2794    def test_overvoltage_faults(self, serial_watcher: SerialWatcher):
2795        """
2796        | Description          | Test over voltage faults                                             |
2797        | :------------------- | :--------------------------------------------------------------------- |
2798        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2799        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2800jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2801        | MIL-PRF Sections     | 4.6.1 (Standard Charge)                                                |
2802        | Instructions         | 1. Rest at 0 mA, 3.8002 V                                         </br>\
2803                                 2. Charge at 100 mA, 4.21 V                                       </br>\
2804                                 3. Charge at 100 mA, 4.10 V                                       </br>\
2805                                 4. Charge at 100 mA, 4.26 V                                       </br>\
2806                                 5. Charge at 100 mA, 4.10 V                                            |
2807        | Pass / Fail Criteria | ⦁ Prefault over voltage charge is False at 3.8002 V               </br>\
2808                                 ⦁ Fault over voltage charge is False at 3.8002 V                  </br>\
2809                                 ⦁ Permanent disable over votlage charge is False at 3.8002 V      </br>\
2810                                 ⦁ Prefault over voltage charge is True at 4.21 V                  </br>\
2811                                 ⦁ Fault over voltage charge is True at 4.21 V                     </br>\
2812                                 ⦁ Prefault over voltage charge is False at 4.10 V                 </br>\
2813                                 ⦁ Fault over voltage charge is False at 4.10 V                    </br>\
2814                                 ⦁ Permanent disable over voltage is True at 4.26 V                </br>\
2815                                 ⦁ Permanent disable over voltage is False at 4.10 V                    |
2816        | Estimated Duration   | 10 seconds                                                             |
2817        | Note                 | While charging, we need to monitor the voltage of our cells.           \
2818                                 Specifically, if a cell ever goes above 4.205 Volts, we should         \
2819                                 trigger a prefault. If this prefault exsists for more than 3 seconds,  \
2820                                 we then should trigger a full fault. If a cell ever gets to be above   \
2821                                 4.250 volts, we should trigger a permanent fault. If we go under 4.201 \
2822                                 we should be able to clear the fault                                   |
2823        """
2824        logger.write_info_to_report("Testing Overvoltage")
2825        assert _bms.load and _bms.charger  # Confirm hardware is available
2826        test_cell = _bms.cells[1]
2827        for cell in _bms.cells.values():
2828            cell.disengage_safety_protocols = True
2829            # Must be high enough to not trigger cell imbalance [abs(high - low) > 0.5V]
2830            self.set_exact_volts(cell, 4.0, 0)
2831
2832        logger.write_result_to_html_report(f"Resting at 0 mA, {CELL_VOLTAGE} V")
2833        serial_watcher.assert_true("flags.prefault_overvoltage_charge", False, 1)
2834        serial_watcher.assert_true("flags.fault_overvoltage_charge", False, 1)
2835        serial_watcher.assert_true("flags.permanentdisable_overvoltage", False, 1)
2836
2837        with _bms.charger(16.8, 0.200):
2838            logger.write_result_to_html_report("Charging at 100 mA, 4.205+ V")
2839            self.set_exact_volts(test_cell, 4.22, 0)
2840            serial_watcher.assert_true("flags.prefault_overvoltage_charge", True, 2)
2841            serial_watcher.assert_true("flags.fault_overvoltage_charge", True, 2)
2842
2843            logger.write_result_to_html_report("Charging at 100 mA, 4.10 V")
2844            self.set_exact_volts(test_cell, 4.10, 0)
2845            serial_watcher.assert_true("flags.prefault_overvoltage_charge", False, 3)
2846            serial_watcher.assert_true("flags.fault_overvoltage_charge", False, 3)
2847
2848            logger.write_result_to_html_report("Charging at 100 mA, 4.25+ V")
2849            self.set_exact_volts(test_cell, 4.27, 0)
2850            serial_watcher.assert_true("flags.permanentdisable_overvoltage", True, 2)
2851
2852            logger.write_result_to_html_report("Charging at 100 mA, 4.10 V")
2853            self.set_exact_volts(test_cell, 4.10, 0)
2854            serial_watcher.assert_false("flags.permanentdisable_overvoltage", False, 3)
2855
2856    def test_undervoltage_faults(self, serial_watcher: SerialWatcher):
2857        """
2858        | Description          | Test under voltage faults                                             |
2859        | :------------------- | :--------------------------------------------------------------------- |
2860        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2861        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2862jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2863        | MIL-PRF Sections     | 4.6.1 (Standard Charge)                                                |
2864        | Instructions         | 1. Rest at 0 mA, 3.0 V                                            </br>\
2865                                 2. Rest at 0 mA, 2.3 V                                            </br>\
2866                                 3. Rest at 0 mA, 2.6 V                                            </br>\
2867                                 4. Charge at 500 mA, 2.3 V                                        </br>\
2868                                 5. Charge at 500 mA, 2.4 V                                        </br>\
2869                                 6. Charge at 500 mA, 2.2 V                                        </br>\
2870                                 5. Charge at 500 mA, 2.6 V                                             |
2871        | Pass / Fail Criteria | ⦁ Prefault under voltage discharge is False at 0mA, 3.0 V         </br>\
2872                                 ⦁ Fault slumber under voltage discharge is False at 0mA, 3.0 V    </br>\
2873                                 ⦁ Prefault under voltage charge is False at 0mA, 3.0 V            </br>\
2874                                 ⦁ Permanent disable under voltage is False at 0mA, 3.0 V          </br>\
2875                                 ⦁ Prefault under voltage discharge is True when resting at 0mA, 2.3 V  </br>\
2876                                 ⦁ Fault slumber under voltage is True when resting at 0mA, 2.3 V  </br>\
2877                                 ⦁ Prefault under voltage discharge is False when resting at 0mA, 2.6 V </br>\
2878                                 ⦁ Fault slumber under voltage is False when resting at 0mA, 2.6 V </br>\
2879                                 ⦁ Prefault under voltage charge is True when charging at 500mA, 2.3 V  </br>\
2880                                 ⦁ Fault slumber under voltage is True when charging at 500mA, 2.3 V    </br>\
2881                                 ⦁ Prefault under voltage discharge is False when charging at 500mA, 2.4 V  </br>\
2882                                 ⦁ Fault slumber under voltage is False when charging at 500mA, 2.4 V   </br>\
2883                                 ⦁ Permanent disable under voltage is True when charging at 500mA, 2.2 V    </br>\
2884                                 ⦁ Permanent disable under voltage is False when charging at 500mA, 2.6 V   |
2885        | Estimated Duration   | 10 seconds                                                             |
2886        | Note                 | This has also been validated in software, meaning the logic should     \
2887                                 properly handle a situation with a cell discharging too low, however   \
2888                                 this has not yet been tested in hardware with a cell sensor reading    \
2889                                 that low of voltage and triggering a fault. If we are reading less     \
2890                                 than 20mamps from the cells, we should be able to trigger an           \
2891                                 under-voltage fault. If we read less than 2.4 volts, we must           \
2892                                 trigger a fault. If this fault persists for over 1 second, we          \
2893                                 should then trigger a full fault. We will not clear this fault         \
2894                                 unless we are able to read above 2.5 volts. If we are reading over     \
2895                                 20 mamps and a cell reads less than 2.325 volts, we must trigger a     \
2896                                 cell voltage charge min prefault, if this persists for another bms     \
2897                                 software cycle we will trigger a full fault. This fault will clear     \
2898                                 when we read above this voltage. If the cell voltage ever goes under   \
2899                                 2.3 while charging, we must trigger a permanent fault.                 |
2900        """
2901        logger.write_info_to_report("Testing Undervoltage")
2902        assert _bms.load and _bms.charger  # Confirm hardware is available
2903        test_cell = _bms.cells[1]
2904        for cell in _bms.cells.values():
2905            cell.disengage_safety_protocols = True
2906            # Must be low enough to not trigger cell imbalance [abs(high - low) > 0.5V]
2907            self.set_exact_volts(cell, 2.6, 0.00)
2908
2909        logger.write_result_to_html_report("Resting at 0 mA, 3.0 V")
2910        serial_watcher.assert_true("flags.prefault_undervoltage_discharge", False, 1)
2911        serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False, 1)
2912        serial_watcher.assert_true("flags.prefault_undervoltage_charge", False, 1)
2913        serial_watcher.assert_true("flags.fault_undervoltage_charge", False, 1)
2914        serial_watcher.assert_true("flags.permanentdisable_undervoltage", False, 1)
2915
2916        with _bms.load(0.500):
2917            logger.write_result_to_html_report("Discharging at -500 mA, 2.325 V")
2918            self.set_exact_volts(test_cell, 2.320, 0.00)
2919            serial_watcher.assert_true("flags.prefault_undervoltage_discharge", True, 2, wait_time=600)
2920            serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", True, 2, wait_time=600)
2921
2922            logger.write_result_to_html_report("Discharging at -500 mA, 2.6 V")
2923            self.set_exact_volts(test_cell, 2.6, 0.00)
2924            serial_watcher.assert_true("flags.prefault_undervoltage_discharge", False, 3, wait_time=600)
2925            serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False, 3, wait_time=600)
2926
2927        with _bms.charger(16.8, 0.500):
2928            logger.write_result_to_html_report("Charging at 500 mA, 2.325 V")
2929            self.set_exact_volts(test_cell, 2.320, 0.00)
2930            serial_watcher.assert_true("flags.prefault_undervoltage_charge", True, 2, wait_time=600)
2931            serial_watcher.assert_true("flags.fault_undervoltage_charge", True, 2, wait_time=600)
2932
2933            logger.write_result_to_html_report("Charging at 500 mA, 2.4 V")
2934            self.set_exact_volts(test_cell, 2.6, 0.00)
2935            serial_watcher.assert_true("flags.prefault_undervoltage_charge", False, 3)
2936            serial_watcher.assert_true("flags.fault_undervoltage_charge", False, 3)
2937
2938            logger.write_result_to_html_report("Charging at 500 mA, 2.2 V")
2939            self.set_exact_volts(test_cell, 2.2, 0.00)
2940            serial_watcher.assert_true("flags.permanentdisable_undervoltage", True, 2)
2941
2942            logger.write_result_to_html_report("Charging at 500 mA, 2.6 V")
2943            self.set_exact_volts(test_cell, 2.6, 0.00)
2944            serial_watcher.assert_false("flags.permanentdisable_undervoltage", False, 3)
2945
2946    def test_cell_imbalance(self, serial_watcher: SerialWatcher):
2947        """
2948        | Description          | Test Cell Imbalance                                                    |
2949        | :------------------- | :--------------------------------------------------------------------- |
2950        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2951        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2952jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2953        | MIL-PRF Sections     | 4.7.2.1 (Cell balance)                                                 |
2954        | Instructions         | 1. Rest at 0 mA, 3.8 V                                            </br>\
2955                                 2. Rest at 0 mA, 2.0 V                                            </br>\
2956                                 3. Rest at 0 mA, 3.8 V                                                 |
2957        | Pass / Fail Criteria | ⦁ Prefault cell imbalance is False after resting at 0mA, 3.8 V    </br>\
2958                                 ⦁ Permanent disable cell imbalance is False after resting at 0mA, 3.8 V    </br>\
2959                                 ⦁ Prefault cell imbalance is True after resting at 0mA, 2.0 V     </br>\
2960                                 ⦁ Permanent disable cell imbalance is True after resting at 0mA, 2.0 V    </br>\
2961                                 ⦁ Permanent disable cell imbalance is False after resting at 0mA, 3.8 V |
2962        | Estimated Duration   | 10 seconds                                                             |
2963        | Note                 | Occurs when the difference between the highest and lowest cell is 0.5V.|
2964        """
2965        logger.write_info_to_report("Testing Cell Imbalance")
2966        assert _bms.load and _bms.charger  # Confirm hardware is available
2967        test_cell = _bms.cells[1]
2968        test_cell.disengage_safety_protocols = True
2969
2970        # Set all cell voltages
2971        for cell in _bms.cells.values():
2972            cell.volts = CELL_VOLTAGE
2973
2974        voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
2975        logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
2976        serial_watcher.assert_true("flags.permanentdisable_cellimbalance", False, 1)
2977
2978        self.set_exact_volts(test_cell, 2.0)
2979        voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
2980        logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
2981        serial_watcher.assert_true("flags.permanentdisable_cellimbalance", True, 2)
2982
2983        self.set_exact_volts(test_cell, CELL_VOLTAGE)
2984        voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
2985        logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
2986        serial_watcher.assert_false("flags.permanentdisable_cellimbalance", False, 3)
2987
2988    def test_overvoltage_overtemp_faults(self, serial_watcher: SerialWatcher):
2989        """
2990        | Description          | Test combined high temperature & high voltage                          |
2991        | :------------------- | :--------------------------------------------------------------------- |
2992        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2993        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2994jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2995        | MIL-PRF Sections     | 4.7.3.3 (Extreme high temperature discharge)                           |
2996        | Instructions         | 1. Rest at 0 mA, 3.8002 V, 15°C                                   </br>\
2997                                 2. Charge at 100 mA, 4.21 V, 54°C                                 </br>\
2998                                 3. Charge at 100 mA, 4.10 V, 15°C                                 </br>\
2999                                 4. Charge at 100 mA, 4.26 V, 94°C                                 </br>\
3000                                 5. Charge at 100 mA, 4.10 V, 15°C                                      |
3001        | Pass / Fail Criteria | ⦁ Prefault over voltage charge is False at 0 mA, 3.8002 V, 15°C   </br>\
3002                                 ⦁ Fault over voltage charge is False at 0 mA, 3.8002 V, 15°C      </br>\
3003                                 ⦁ Permanent disable over votlage charge is False at 0 mA, 3.8002 V, 15°C </br>\
3004                                 ⦁ Prefault over voltage charge is True at 4.21 V, 54°C            </br>\
3005                                 ⦁ Fault over voltage charge is True at 4.21 V, 54°C               </br>\
3006                                 ⦁ Prefault over temperature charge is True at 4.21 V, 54°C        </br>\
3007                                 ⦁ Fault over temperature charge is True at 4.21 V, 54°C           </br>\
3008                                 ⦁ Prefault over voltage charge is False at 4.10 V, 15°C           </br>\
3009                                 ⦁ Prefault over temperature charge is False at 4.10 V, 15°C       </br>\
3010                                 ⦁ Fault over voltage charge is False at 4.10 V, 15°C              </br>\
3011                                 ⦁ Fault over temperature charge is False at 4.10 V                </br>\
3012                                 ⦁ Permanent disable over voltage is True at 4.26 V, 94°C          </br>\
3013                                 ⦁ Permanent disable over temperature is True at 4.26 V, 94°C      </br>\
3014                                 ⦁ Permanent disable over voltage is False at  4.10 V, 15°C        </br>\
3015                                 ⦁ Permanent disable over temperature is False at  4.10 V, 15°C         |
3016        | Estimated Duration   | 10 seconds                                                             |
3017        | Note                 | Combine over voltage and over temperature faults                       |
3018        """
3019        logger.write_info_to_report("Testing Overvoltage with Overtemp")
3020        assert _bms.load and _bms.charger  # Confirm hardware is available
3021        test_cell = _bms.cells[1]
3022        for cell in _bms.cells.values():
3023            cell.disengage_safety_protocols = True
3024            # Must be high enough to not trigger cell imbalance [abs(high - low) > 0.5V]
3025            self.set_exact_volts(cell, 4.0, 0.0)
3026
3027        logger.write_result_to_html_report(f"Resting at 0 mA, {CELL_VOLTAGE} V, 15°C")
3028        serial_watcher.assert_true("flags.prefault_overvoltage_charge", False, 1)
3029        serial_watcher.assert_true("flags.fault_overvoltage_charge", False, 1)
3030        serial_watcher.assert_true("flags.permanentdisable_overvoltage", False, 1)
3031
3032        serial_watcher.assert_true("flags.prefault_overtemp_charge", False, 1)
3033        serial_watcher.assert_true("flags.fault_overtemp_charge", False, 1)
3034        serial_watcher.assert_true("flags.permanentdisable_overtemp", False, 1)
3035
3036        with _bms.charger(16.8, 0.200):
3037            logger.write_result_to_html_report("Charging at 100 mA, 4.21 V, 54°C")
3038            self.set_exact_volts(test_cell, 4.22, 0.0)
3039            _plateset.thermistor1 = 54
3040            serial_watcher.assert_true("flags.prefault_overvoltage_charge", True, 2)
3041            serial_watcher.assert_true("flags.fault_overvoltage_charge", True, 2)
3042            serial_watcher.assert_true("flags.prefault_overtemp_charge", True, 2)
3043            serial_watcher.assert_true("flags.fault_overtemp_charge", True, 2)
3044
3045            logger.write_result_to_html_report("Charging at 100 mA, 4.10 V, 15°C")
3046            self.set_exact_volts(test_cell, 4.10, 0.0)
3047            _plateset.thermistor1 = 15
3048            serial_watcher.assert_true("flags.prefault_overvoltage_charge", False, 3)
3049            serial_watcher.assert_true("flags.fault_overvoltage_charge", False, 3)
3050            serial_watcher.assert_true("flags.prefault_overtemp_charge", False, 3)
3051            serial_watcher.assert_true("flags.fault_overtemp_charge", False, 3)
3052
3053    def test_undervoltage_undertemp_faults(self, serial_watcher: SerialWatcher):
3054        """
3055        | Description          | Test combined low temperature & low voltage                            |
3056        | :------------------- | :--------------------------------------------------------------------- |
3057        | GitHub Issue         | turnaroundfactor/HITL#476                                              |
3058        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3059jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
3060        | MIL-PRF Sections     | 4.7.3.2 (Extreme low temperature Discharge)                            |
3061        | Instructions         | 1. Rest at 0 mA, 3.0 V, 15°C                                      </br>\
3062                                 2. Rest at 0 mA, 2.3 V, -21°C                                     </br>\
3063                                 3. Rest at 0 mA, 2.6 V, -17°C                                     </br>\
3064                                 4. Charge at 500 mA, 2.3 V, -25°C                                  </br>\
3065                                 5. Charge at 500 mA, 2.4 V, -20°C                                  </br>\
3066                                 6. Charge at 500 mA, 2.2 V,  15°C                                 </br>\
3067                                 7. Charge at 500 mA, 2.6 V,  15°C                                      |
3068        | Pass / Fail Criteria | ⦁ Prefault under voltage discharge is False at 0 mA, 3.0 V, 15°C  </br>\
3069                                 ⦁ Fault slumber under voltage charge is False at 0 mA, 3.0 V, 15°C</br>\
3070                                 ⦁ Prefault under voltage charge is False at 0 mA, 3.0 V, 15°C     </br>\
3071                                 ⦁ Fault under voltage charge is False at 0 mA, 3.0 V, 15°C        </br>\
3072                                 ⦁ Permanent disable under voltage is False at 0 mA, 3.0 V, 15°C   </br>\
3073                                 ⦁ Prefault under temperature discharge is False at 0 mA, 3.0 V, 15°C </br>\
3074                                 ⦁ Fault under temperature discharge is False at 0 mA, 3.0 V, 15°C </br>\
3075                                 ⦁ Prefault under temperature charge is False at 0 mA, 3.0 V, 15°C </br>\
3076                                 ⦁ Fault under temperature charge is False at 0 mA, 3.0 V, 15°C    </br>\
3077                                 ⦁ Fault under voltage charge is False at 0 mA, 3.0 V, 15°C        </br>\
3078                                 ⦁ Fault under voltage charge is False at 0 mA, 3.0 V, 15°C        </br>\
3079                                 ⦁ Prefault under voltage discharge is True at 0 mA, 2.3 V, -21°C  </br>\
3080                                 ⦁ Fault slumber under voltage discharge is True at 0 mA, 2.3 V, -21°C </br>\
3081                                 ⦁ Prefault under temperature discharge is True at 0 mA, 2.3 V, -21°C  </br>\
3082                                 ⦁ Fault under temperature discharge is True at 0 mA, 2.3 V, -21°C </br>\
3083                                 ⦁ Prefault under voltage discharge is False at 0 mA, 2.6 V, -17°C </br>\
3084                                 ⦁ Fault slumber under voltage discharge is False at 0 mA, 2.6 V, -17°C </br>\
3085                                 ⦁ Prefault under temperature discharge is False at 0 mA, 2.6 V, -17°C  </br>\
3086                                 ⦁ Fault under temperature discharge is False at 0 mA, 2.6 V, -17°C</br>\
3087                                 ⦁ Prefault under voltage charge is True at 500 mA, 2.3 V, -25°C    </br>\
3088                                 ⦁ Fault slumber under voltage charge is True at 500 mA, 2.3 V, -25°C</br>\
3089                                 ⦁ Prefault under temperature charge is True at 500 mA, 2.3 V, -25°C</br>\
3090                                 ⦁ Fault under temperature charge is True at 500 mA, 2.3 V, -25°C   </br>\
3091                                 ⦁ Prefault under voltage charge is False at 500 mA, 2.4 V, -20°C    </br>\
3092                                 ⦁ Fault slumber under voltage charge is False at 500 mA, 2.4 V, -20°C </br>\
3093                                 ⦁ Prefault under temperature charge is False at 500 mA, 2.4 V, -20°C</br>\
3094                                 ⦁ Fault under temperature charge is False at 500 mA, 2.4 V, -20°C   </br>\
3095                                 ⦁ Permanent disable under voltage is True at 500 mA, 2.2 V, 15°C  </br>\
3096                                 ⦁ Permanent disable under voltage is False at 500 mA, 2.6 V, 15°C      |
3097        | Estimated Duration   | 10 seconds                                                             |
3098        | Note                 | Combine under voltage and under temperature faults                     |
3099        """
3100
3101        logger.write_info_to_report("Testing Undervoltage with Undertemp")
3102        assert _bms.load and _bms.charger  # Confirm hardware is available
3103        test_cell = _bms.cells[1]
3104        for cell in _bms.cells.values():
3105            cell.disengage_safety_protocols = True
3106            self.set_exact_volts(
3107                cell, 2.8, 0
3108            )  # Must be low enough to not trigger cell imbalance [abs(high - low) > 0.5V]
3109
3110        logger.write_result_to_html_report("Resting at 0 mA, 3.0 V, 15°C")
3111        _plateset.thermistor1 = 15
3112        serial_watcher.assert_true("flags.prefault_undervoltage_discharge", False, 1)
3113        serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False, 1)
3114        serial_watcher.assert_true("flags.prefault_undervoltage_charge", False, 1)
3115        serial_watcher.assert_true("flags.fault_undervoltage_charge", False, 1)
3116        serial_watcher.assert_true("flags.permanentdisable_undervoltage", False, 1)
3117
3118        serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 1)
3119        serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 1)
3120        serial_watcher.assert_true("flags.prefault_undertemp_charge", False, 1)
3121        serial_watcher.assert_true("flags.fault_undertemp_charge", False, 1)
3122
3123        with _bms.load(0.500):
3124            logger.write_result_to_html_report("Discharging at -500 mA, 2.325 V, -35°C")
3125            self.set_exact_volts(test_cell, 2.320, 0)
3126            _plateset.thermistor1 = -35
3127            serial_watcher.assert_true("flags.prefault_undervoltage_discharge", True, 2)
3128            serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", True, 2)
3129            serial_watcher.assert_true("flags.prefault_undertemp_discharge", True, 2)
3130            serial_watcher.assert_true("flags.fault_undertemp_discharge", True, 2)
3131
3132            logger.write_result_to_html_report("Discharging at -500 mA, 2.6 V, -30°C")
3133            self.set_exact_volts(test_cell, 2.8, 0)
3134            _plateset.thermistor1 = -30
3135            serial_watcher.assert_true("flags.prefault_undervoltage_discharge", False, 3)
3136            serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False, 3)
3137            serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 3)
3138            serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 3)
3139
3140        with _bms.charger(16.8, 0.500):
3141            logger.write_result_to_html_report("Charging at 500 mA, 2.325 V, -25°C")
3142            self.set_exact_volts(test_cell, 2.320, 0.00)
3143            _plateset.thermistor1 = -25
3144            serial_watcher.assert_true("flags.prefault_undervoltage_charge", True, 2)
3145            serial_watcher.assert_true("flags.fault_undervoltage_charge", True, 2)
3146            serial_watcher.assert_true("flags.prefault_undertemp_charge", True, 2)
3147            serial_watcher.assert_true("flags.fault_undertemp_charge", True, 2)
3148
3149            logger.write_result_to_html_report("Charging at 500 mA, 2.6 V, -20°C")
3150            self.set_exact_volts(test_cell, 2.8, 0)
3151            _plateset.thermistor1 = -20
3152            serial_watcher.assert_true("flags.prefault_undervoltage_charge", False, 3)
3153            serial_watcher.assert_true("flags.fault_undervoltage_charge", False, 3)
3154            serial_watcher.assert_true("flags.prefault_undertemp_charge", False, 3)
3155            serial_watcher.assert_true("flags.fault_undertemp_charge", False, 3)
3156
3157    def test_cell_imbalance_charge(self, serial_watcher: SerialWatcher):
3158        """
3159        | Description          | Test Cell Imbalance                                                    |
3160        | :------------------- | :--------------------------------------------------------------------- |
3161        | GitHub Issue         | turnaroundfactor/HITL#476                                              |
3162        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3163jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
3164        | MIL-PRF Sections     | 4.7.2.1 (Cell balance)                                                 |
3165        | Instructions         | 1. Rest at 0 mA                                                   </br>\
3166                                 2. Charge at 1 mA                                                 </br>\
3167                                 3. Rest at 0 mA                                                        |
3168        | Pass / Fail Criteria | ⦁ Prefault cell imbalance is False after resting at 0mA           </br>\
3169                                 ⦁ Permanent disable cell imbalance is False after resting at 0mA  </br>\
3170                                 ⦁ Prefault cell imbalance is True after resting at 1 A            </br>\
3171                                 ⦁ Permanent disable cell imbalance is True after resting 1 A      </br>\
3172                                 ⦁ Prefault cell imbalance is True after resting at 0 A            </br>\
3173                                 ⦁ Permanent disable cell imbalance is False after resting at 0 A       |
3174        | Estimated Duration   | 10 seconds                                                             |
3175        | Note                 | Occurs when the difference between the highest and lowest cell is 0.5V.|
3176        """
3177        logger.write_info_to_report("Testing Cell Imbalance Charge")
3178        assert _bms.load and _bms.charger  # Confirm hardware is available
3179
3180        test_cell = _bms.cells[1]
3181        test_cell.disengage_safety_protocols = True
3182        for cell in _bms.cells.values():
3183            cell.exact_volts = 4.2
3184
3185        voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
3186        logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
3187        serial_watcher.assert_true("flags.permanentdisable_cellimbalance", False, 1)
3188
3189        with _bms.charger(16.8, 1):
3190            self.set_exact_volts(test_cell, 2.5)
3191            voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
3192            logger.write_result_to_html_report(f"Charging at 1 A, {', '.join(voltages)}")
3193            serial_watcher.assert_true("flags.permanentdisable_cellimbalance", True)
3194
3195            test_cell.exact_volts = 4.2
3196            voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
3197            logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
3198            serial_watcher.assert_false("flags.permanentdisable_cellimbalance", True)
3199
3200    def test_current_limit(self, serial_watcher: SerialWatcher):
3201        """
3202        | Description          | Confirm A fault is raised after 3.5A                                                 |
3203        | :------------------- | :----------------------------------------------------------------------------------- |
3204        | GitHub Issue         | turnaroundfactor/HITL#476                                                            |
3205        | Instructions         | 1. Rest for 30 second                                                           </br>\
3206                                 2. Charge at 3.75 amps (the max limit will be 3.5 amps)                         </br>\
3207                                 3. Verify a prefault and fault are raised.                                      </br>\
3208                                 4. Rest until faults clear                                                      </br>\
3209                                 5. Charge at 3.75 amps                                                          </br>\
3210                                 6. Verify a prefault occurred but not a fault                                        |
3211        | Pass / Fail Criteria | Pass if faults are raised                                                            |
3212        | Estimated Duration   | 2 minutes                                                                            |
3213        | Notes                | We use 3.5A as a limit due to hardware limitations                                   |
3214        """
3215
3216        logger.write_info_to_report("Testing Current Limit")
3217        assert _bms.load and _bms.charger  # Confirm hardware is available
3218
3219        # Rest and make sure no faults are active
3220        logger.write_result_to_html_report("Confirm no faults are active")
3221        serial_watcher.assert_true("flags.prefault_overcurrent_charge", False, 1)
3222        serial_watcher.assert_true("flags.fault_overcurrent_charge", False, 1)
3223
3224        # Charge at 3.25 amps and verify faults are raised
3225        logger.write_result_to_html_report("Charging 3.75 A")
3226        with _bms.charger(16.8, 3.75):
3227            serial_watcher.assert_true("flags.prefault_overcurrent_charge", True, 2, wait_time=60)
3228            serial_watcher.assert_true("flags.fault_overcurrent_charge", True, 2, wait_time=60)
3229
3230        # Rest until faults clear
3231        logger.write_result_to_html_report("Resting")
3232        serial_watcher.assert_true("flags.prefault_overcurrent_charge", False, 3)
3233        serial_watcher.assert_true("flags.fault_overcurrent_charge", False, 3)
3234        assert (
3235            serial_watcher.events["flags.fault_overcurrent_charge"][2].bms_time
3236            - serial_watcher.events["flags.fault_overcurrent_charge"][1].bms_time
3237        ).total_seconds() >= 60
3238
3239    def test_undertemp_charge_rate(self, serial_watcher: SerialWatcher):
3240        """
3241        | Description          | Confirm a fault is raised at or above 3.1A when below 5°C                            |
3242        | :------------------- | :----------------------------------------------------------------------------------- |
3243        | GitHub Issue         | turnaroundfactor/HITL#611                                                            |
3244        | Instructions         | 1. Rest for 30 second                                                           </br>\
3245                                 2. Charge at 3.3 amps below 5°C                                                 </br>\
3246                                 3. Verify a prefault (before 1 second) and fault (after 1 second) are raised.   </br>\
3247                                 4. Charge above 7°C until faults clear                                               |
3248        | Pass / Fail Criteria | Pass if faults are raised                                                            |
3249        | Estimated Duration   | 2 minutes                                                                            |
3250        """
3251
3252        logger.write_info_to_report("Testing Undertemp charge rate")
3253        assert _bms.load and _bms.charger  # Confirm hardware is available
3254
3255        # Rest and make sure no faults are active
3256        logger.write_result_to_html_report("Confirm no faults are active")
3257        serial_watcher.assert_true("flags.prefault_undertemp_charge_rate", False, 1)
3258        serial_watcher.assert_true("flags.fault_undertemp_charge_rate", False, 1)
3259
3260        # Discharge at 3.1+ amps and verify faults are raised
3261        logger.write_result_to_html_report("Discharging at 3.1A+")
3262        with _bms.charger(16.8, 3.3):
3263            _plateset.thermistor1 = 3
3264            serial_watcher.assert_true("flags.prefault_undertemp_charge_rate", True, 2, wait_time=60)
3265            serial_watcher.assert_true("flags.fault_undertemp_charge_rate", True, 2, wait_time=60)
3266            _plateset.thermistor1 = 8
3267            serial_watcher.assert_true("flags.prefault_undertemp_charge_rate", False, 3, wait_time=90)
3268            serial_watcher.assert_true("flags.fault_undertemp_charge_rate", False, 3, wait_time=90)
3269
3270    def test_overcurrent_discharge_sustained(self, serial_watcher: SerialWatcher):
3271        """
3272        | Description          | Test Over Current Discharge Sustained Fault                            |
3273        | :------------------- | :--------------------------------------------------------------------- |
3274        | GitHub Issue         | turnaroundfactor/HITL#762                                              |
3275        | Instructions         | 1. Check prefault_sw_overcurrent_discharge and fault_sw_overcurrent_discharge </br>\
3276                                 2. Set current to less than -2.5 amps                             </br>\
3277                                 3. Confirm faults listed in #1 are true                           </br>\
3278                                 4. Set current to 0 amps (rest)                                   </br>\
3279                                 5. Confirm faults listed in #1 are false                               |
3280        | Pass / Fail Criteria | ⦁ Prefault overCurrent Discharge Sustained is False at start      </br>\
3281                                 ⦁ Fault overCurrent Discharge Sustained is False at start         </br>\
3282                                 ⦁ Prefault overCurrent Discharge Sustained is True when current is -2.5 A </br>\
3283                                 ⦁ Fault overCurrent Discharge Sustained is True when current is -2.5 A  </br>\
3284                                 ⦁ Prefault overCurrent Discharge Sustained is False when current is 0 A </br>\
3285                                 ⦁ Fault overCurrent Discharge Sustained is False when current is 0 A   |
3286        | Estimated Duration   | 10 seconds                                                             |
3287        """
3288
3289        logger.write_info_to_report("Testing Overcurrent Discharge Sustained Faults")
3290        assert _bms.load and _bms.charger  # Confirm hardware is available
3291
3292        logger.write_info_to_report("Sw overcurrent protection high current short time")
3293        serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_2", False, 1)
3294        serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_2", False, 1)
3295
3296        logger.write_result_to_html_report("Setting current to less than -2.6 amps")
3297        with _bms.load(2.6):
3298            serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_2", True, 2)
3299            serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_2", True, 2)
3300
3301        logger.write_result_to_html_report("Resting")
3302        serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_2", False, 3, wait_time=60 * 25)
3303        serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_2", False, 3, wait_time=60 * 25)
3304
3305        logger.write_info_to_report("Sw overcurrent protection low current longer time")
3306        serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_1", False, 1)
3307        serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_1", False, 1)
3308
3309        logger.write_result_to_html_report("Setting current to less than -2.4 amps")
3310        with _bms.load(2.4):
3311            serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_1", True, 2)
3312            serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_1", True, 2)
3313
3314        logger.write_result_to_html_report("Resting")
3315        serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_1", False, 3, wait_time=60 * 10)
3316        serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_1", False, 3, wait_time=60 * 10)

Test all faults

def test_wakeup_1( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
2556    def test_wakeup_1(self, serial_watcher: SerialWatcher):
2557        """
2558        | Description          | Test Wakeup 1                                                          |
2559        | :------------------- | :--------------------------------------------------------------------- |
2560        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2561        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2562jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2563        | MIL-PRF Sections     | 3.6.8.2 (Battery wake up)                                              |
2564        | Instructions         | 1. Set Default state at 0mA                                       </br>\
2565                                 2. Discharge at 100 mA (below -46 mA)                             </br>\
2566                                 3. Discharge at 10 mA (between -46mA and 19ma)                    </br>\
2567                                 4. Charge at 100 mA, (above 19 mA)                                     |
2568        | Pass / Fail Criteria | ⦁ n_wakeup_gpio = 1, with default state at 0 mA                   </br>\
2569                                 ⦁ n_wakeup_gpio = 0, with default state at 100 mA                   </br>\
2570                                 ⦁ n_wakeup_gpio = 1, with default state at 10 mA                   </br>\
2571                                 ⦁ n_wakeup_gpio = 0, with default state at 0 mA                        |
2572        | Estimated Duration   | 10 seconds                                                             |
2573        | Note                 | The BMS should be in a state of slumber if the current measured is     \
2574                                 between -46mA and 19ma. This is done using internal comparators on the \
2575                                 board to a logical AND chip feeding into an interrupt pin. To test     \
2576                                 this, 3 different currents should be set. One current below -46ma,     \
2577                                 another current between -46mA and 19ma, and another current above      \
2578                                 19ma. If the current is within the allowable range, we should read     \
2579                                 logic 1 on the N_WAKEUP pin. If the current is outside (above or       \
2580                                 below) we should read logic 0 on the pin.                              |
2581        """
2582
2583        logger.write_info_to_report("Testing Wakeup")
2584        assert _bms.load and _bms.charger  # Confirm hardware is available
2585
2586        logger.write_result_to_html_report("Default state")
2587        serial_watcher.assert_true("n_wakeup_gpio", True, 1)
2588
2589        logger.write_result_to_html_report("Discharging 200 mA")
2590        with _bms.load(0.200):
2591            serial_watcher.assert_true("n_wakeup_gpio", False, 2)
2592
2593        logger.write_result_to_html_report("Discharging 10 mA")
2594        with _bms.load(0.010):
2595            serial_watcher.assert_true("n_wakeup_gpio", True, 3)
2596
2597        logger.write_result_to_html_report("Charging 200 mA")
2598        with _bms.charger(16.8, 0.200):
2599            serial_watcher.assert_true("n_wakeup_gpio", False, 4)
Description Test Wakeup 1
GitHub Issue turnaroundfactor/HITL#476
Google Docs Google Sheet Cell
MIL-PRF Sections 3.6.8.2 (Battery wake up)
Instructions 1. Set Default state at 0mA
2. Discharge at 100 mA (below -46 mA)
3. Discharge at 10 mA (between -46mA and 19ma)
4. Charge at 100 mA, (above 19 mA)
Pass / Fail Criteria ⦁ n_wakeup_gpio = 1, with default state at 0 mA
⦁ n_wakeup_gpio = 0, with default state at 100 mA
⦁ n_wakeup_gpio = 1, with default state at 10 mA
⦁ n_wakeup_gpio = 0, with default state at 0 mA
Estimated Duration 10 seconds
Note The BMS should be in a state of slumber if the current measured is between -46mA and 19ma. This is done using internal comparators on the board to a logical AND chip feeding into an interrupt pin. To test this, 3 different currents should be set. One current below -46ma, another current between -46mA and 19ma, and another current above 19ma. If the current is within the allowable range, we should read logic 1 on the N_WAKEUP pin. If the current is outside (above or below) we should read logic 0 on the pin.
def test_overtemp_fault( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
2601    def test_overtemp_fault(self, serial_watcher: SerialWatcher):
2602        """
2603        | Description          | Test over temperature Fault                                             |
2604        | :------------------- | :--------------------------------------------------------------------- |
2605        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2606        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2607jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2608        | MIL-PRF Sections     | 4.7.3.3 (Extreme high temperature discharge)                           |
2609        | Instructions         | 1. Set Default state at 15                                        </br>\
2610                                 2. Resting at 59°C (at or above 59°C)                             </br>\
2611                                 3. Discharging at 100 mA, 59°C (at or above 59°C)                 </br>\
2612                                 4. Charging at 100 mA, 54°C (at or above 53°C)                    </br>\
2613                                 5. Charging at 100 mA, 94°C (at or above 93°C)                    </br>\
2614                                 6. Discharging at 100 mA, 94°C (at or above 93°C)                 </br>\
2615                                 7. Rest at 94°C (at or above 93°C)                                      |
2616        | Pass / Fail Criteria | ⦁ Prefault overtemp Discharge value is False (default state)       </br>\
2617                                 ⦁ Fault overtemp Discharge value is False (default state)          </br>\
2618                                 ⦁ Prefault overtemp charge value is False (default state)          </br>\
2619                                 ⦁ Fault overtemp charge value is False (default state)             </br>\
2620                                 ⦁ Permanent Disable overtemp is False (default state)              </br>\
2621                                 ⦁ Measure output fets disabled is  False (default state)           </br>\
2622                                 ⦁ Resting overtemp discharge is True for fault & prefault          </br>\
2623                                 ⦁ Discharging overtemp value is True for fault & prefault          </br>\
2624                                 ⦁ After charging permanent disable, permanent disable overtemp is True </br>\
2625                                 ⦁ After resting permanent disable, permanent disable overtemp is True  |
2626        | Estimated Duration   | 12 hours                                                               |
2627        | Note                 | If our batteries get too hot, we must trigger a fault. This is         \
2628                                 common during high discharge or charging cycles. There are 3 different \
2629                                 environments where we would trigger an overtemp fault: charge,         \
2630                                 discharge and resting. While charging, if we are above 53C we must     \
2631                                 trigger a fault If we are resting or discharging at 59 degrees we must \
2632                                 trigger a fault. Both of these faults should trigger a prefault        \
2633                                 condition in our flags. After we cycle again we should then trigger a  \
2634                                 full fault. If the temperature ever goes above 93 degrees, the fault   \
2635                                 should never clear and we should be in permanent fault and trigger     \
2636                                 the fets. (This should also be seen in the flags)                      |
2637        """
2638        logger.write_info_to_report("Testing Overtemp")
2639        assert _bms.load and _bms.charger  # Confirm hardware is available
2640
2641        # Test default state
2642        logger.write_result_to_html_report(f"Resting at 0 mA, {CELL_VOLTAGE} V, 15°C")
2643        _plateset.thermistor1 = 15
2644        serial_watcher.assert_true("flags.prefault_overtemp_discharge", False, 1)
2645        serial_watcher.assert_true("flags.fault_overtemp_discharge", False, 1)
2646        serial_watcher.assert_true("flags.prefault_overtemp_charge", False, 1)
2647        serial_watcher.assert_true("flags.fault_overtemp_charge", False, 1)
2648        serial_watcher.assert_true("flags.permanentdisable_overtemp", False, 1)
2649        serial_watcher.assert_true("flags.measure_output_fets_disabled", False, 1)
2650
2651        # Test resting overtemp
2652        logger.write_result_to_html_report("Resting at 59°C")
2653        _plateset.thermistor1 = 59
2654        serial_watcher.assert_true("flags.prefault_overtemp_discharge", True, 2)
2655        serial_watcher.assert_true("flags.fault_overtemp_discharge", True, 2)
2656        logger.write_result_to_html_report("Resting at 15°C")
2657        _plateset.thermistor1 = 15
2658        serial_watcher.assert_true("flags.prefault_overtemp_discharge", False, 3)
2659        serial_watcher.assert_true("flags.fault_overtemp_discharge", False, 3)
2660
2661        # Test discharging overtemp
2662        logger.write_result_to_html_report("Discharging at -100 mA, 59°C")
2663        with _bms.load(0.100):
2664            _plateset.thermistor1 = 59
2665            serial_watcher.assert_true("flags.prefault_overtemp_discharge", True, 4)
2666            serial_watcher.assert_true("flags.fault_overtemp_discharge", True, 4)
2667            logger.write_result_to_html_report("Discharging at -100 mA, 15°C")
2668            _plateset.thermistor1 = 15
2669            serial_watcher.assert_true("flags.prefault_overtemp_discharge", False, 5)
2670            serial_watcher.assert_true("flags.fault_overtemp_discharge", False, 5)
2671
2672        # Test charging overtemp
2673        logger.write_result_to_html_report("Charging at 100 mA, 54°C")
2674        with _bms.charger(16.8, 0.200):
2675            _plateset.thermistor1 = 54
2676            serial_watcher.assert_true("flags.prefault_overtemp_charge", True, 2)
2677            serial_watcher.assert_true("flags.fault_overtemp_charge", True, 2)
2678            logger.write_result_to_html_report("Charging at 100 mA, 15°C")
2679            _plateset.thermistor1 = 15
2680            serial_watcher.assert_true("flags.prefault_overtemp_charge", False, 3)
2681            serial_watcher.assert_true("flags.fault_overtemp_charge", False, 3)
2682
2683        # Test charging permanent disable
2684        logger.write_result_to_html_report("Charging at 100 mA, 94°C")
2685        _plateset.disengage_safety_protocols = True
2686        _plateset.thermistor1 = 94
2687        _plateset.disengage_safety_protocols = False
2688        with _bms.charger(16.8, 0.200):
2689            serial_watcher.assert_true("flags.permanentdisable_overtemp", True, 2)
2690            logger.write_result_to_html_report("Charging at 100 mA, 15°C")
2691            _plateset.thermistor1 = 15
2692            serial_watcher.assert_false("flags.permanentdisable_overtemp", False, 3)
2693
2694        # Test discharging permanent disable
2695        logger.write_result_to_html_report("Discharging at -100 mA, 94°C")
2696        _plateset.disengage_safety_protocols = True
2697        _plateset.thermistor1 = 94
2698        _plateset.disengage_safety_protocols = False
2699        with _bms.load(0.100):
2700            serial_watcher.assert_true("flags.permanentdisable_overtemp", True, 2)
2701            logger.write_result_to_html_report("Discharging at -100 mA, 15°C")
2702            _plateset.thermistor1 = 15
2703            serial_watcher.assert_false("flags.permanentdisable_overtemp", False, 3)
2704
2705        # Test resting permanent disable
2706        _plateset.disengage_safety_protocols = True
2707        logger.write_result_to_html_report("Resting at 94°C")
2708        _plateset.thermistor1 = 94
2709        _plateset.disengage_safety_protocols = False
2710        serial_watcher.assert_true("flags.permanentdisable_overtemp", True, 2)
2711        logger.write_result_to_html_report("Resting at 15°C")
2712        _plateset.thermistor1 = 15
2713        serial_watcher.assert_false("flags.permanentdisable_overtemp", False, 3)
Description Test over temperature Fault
GitHub Issue turnaroundfactor/HITL#476
Google Docs Google Sheet Cell
MIL-PRF Sections 4.7.3.3 (Extreme high temperature discharge)
Instructions 1. Set Default state at 15
2. Resting at 59°C (at or above 59°C)
3. Discharging at 100 mA, 59°C (at or above 59°C)
4. Charging at 100 mA, 54°C (at or above 53°C)
5. Charging at 100 mA, 94°C (at or above 93°C)
6. Discharging at 100 mA, 94°C (at or above 93°C)
7. Rest at 94°C (at or above 93°C)
Pass / Fail Criteria ⦁ Prefault overtemp Discharge value is False (default state)
⦁ Fault overtemp Discharge value is False (default state)
⦁ Prefault overtemp charge value is False (default state)
⦁ Fault overtemp charge value is False (default state)
⦁ Permanent Disable overtemp is False (default state)
⦁ Measure output fets disabled is False (default state)
⦁ Resting overtemp discharge is True for fault & prefault
⦁ Discharging overtemp value is True for fault & prefault
⦁ After charging permanent disable, permanent disable overtemp is True
⦁ After resting permanent disable, permanent disable overtemp is True
Estimated Duration 12 hours
Note If our batteries get too hot, we must trigger a fault. This is common during high discharge or charging cycles. There are 3 different environments where we would trigger an overtemp fault: charge, discharge and resting. While charging, if we are above 53C we must trigger a fault If we are resting or discharging at 59 degrees we must trigger a fault. Both of these faults should trigger a prefault condition in our flags. After we cycle again we should then trigger a full fault. If the temperature ever goes above 93 degrees, the fault should never clear and we should be in permanent fault and trigger the fets. (This should also be seen in the flags)
def test_undertemp_faults( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
2715    def test_undertemp_faults(self, serial_watcher: SerialWatcher):
2716        """
2717        | Description          | Test under temperature Fault                                             |
2718        | :------------------- | :--------------------------------------------------------------------- |
2719        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2720        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2721jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2722        | MIL-PRF Sections     | 4.7.2.7 (Low Temperature Discharge)                                    |
2723        | Instructions         | 1. Set Default state at 15                                        </br>\
2724                                 2. Resting at -35°C (at or below -33.2°C)                         </br>\
2725                                 3. Discharging at -100 mA, -35°C (at or below -33.2°C)            </br>\
2726                                 4. Discharging at -100 mA, -30°C (at or above -32.2°C)            </br>\
2727                                 5. Charging at 100 mA, -25°C (at or below -23.2°C)                </br>\
2728                                 6. Charging at 100 mA, -20°C (at or above -22.2°C)                     |
2729        | Pass / Fail Criteria | ⦁ Prefault overtemp Discharge value is False (default state)       </br>\
2730                                 ⦁ Fault overtemp Discharge value is False (default state)          </br>\
2731                                 ⦁ Prefault overtemp charge value is False (default state)          </br>\
2732                                 ⦁ Fault overtemp charge value is False (default state)             </br>\
2733                                 ⦁ Permanent Disable overtemp is False (default state)              </br>\
2734                                 ⦁ Measure output fets disabled is  False (default state)           </br>\
2735                                 ⦁ Resting overtemp discharge is True for fault & prefault          </br>\
2736                                 ⦁ Discharging overtemp value is True for fault & prefault          </br>\
2737                                 ⦁ After charging permanent disable, permanent disable overtemp is True </br>\
2738                                 ⦁ After resting permanent disable, permanent disable overtemp is True  |
2739        | Estimated Duration   | 10 seconds                                                             |
2740        | Note                 | This occurs when we read more than 20 mamps from the battery, if any   \
2741                                 of the cells are under 0 degrees Celsius this will trigger a fault.    \
2742                                 This will be cleared if we go above -2C. Regardless of current being   \
2743                                 measured, if we ever read below -20C, this should trigger a fault.     \
2744                                 This fault should not be cleared until we are above -18C               |
2745        """
2746        logger.write_info_to_report("Testing Undertemp")
2747        assert _bms.load and _bms.charger  # Confirm hardware is available
2748
2749        logger.write_result_to_html_report(f"Resting at 0 mA, {CELL_VOLTAGE} V, 15°C")
2750        _plateset.thermistor1 = 15
2751
2752        # Discharging flags
2753        serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 1)
2754        serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 1)
2755
2756        # Charging flags
2757        serial_watcher.assert_true("flags.prefault_undertemp_charge", False, 1)
2758        serial_watcher.assert_true("flags.fault_undertemp_charge", False, 1)
2759
2760        logger.write_result_to_html_report("Resting at -35°C")
2761        _plateset.thermistor1 = -35
2762        serial_watcher.assert_true("flags.prefault_undertemp_discharge", True, 2)
2763        serial_watcher.assert_true("flags.fault_undertemp_discharge", True, 2)
2764        logger.write_result_to_html_report("Resting at -30°C")
2765        _plateset.thermistor1 = -30
2766        serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 3)
2767        serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 3)
2768
2769        logger.write_result_to_html_report("Discharging at -100 mA, -35°C")
2770        with _bms.load(0.100):
2771            _plateset.thermistor1 = -35
2772            serial_watcher.assert_true("flags.prefault_undertemp_discharge", True, 4)
2773            serial_watcher.assert_true("flags.fault_undertemp_discharge", True, 4)
2774            logger.write_result_to_html_report("Discharging at -100 mA, -30°C")
2775            _plateset.thermistor1 = -30
2776            serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 5)
2777            serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 5)
2778
2779        logger.write_result_to_html_report("Charging at 100 mA, -25°C")
2780        with _bms.charger(16.8, 0.200):
2781            _plateset.thermistor1 = -25
2782            serial_watcher.assert_true("flags.prefault_undertemp_charge", True, 2)
2783            serial_watcher.assert_true("flags.fault_undertemp_charge", True, 2)
2784            logger.write_result_to_html_report("Charging at 100 mA, 20°C")
2785            _plateset.thermistor1 = 20
2786            serial_watcher.assert_true("flags.prefault_undertemp_charge", False, 3)
2787            serial_watcher.assert_true("flags.fault_undertemp_charge", False, 3)
Description Test under temperature Fault
GitHub Issue turnaroundfactor/HITL#476
Google Docs Google Sheet Cell
MIL-PRF Sections 4.7.2.7 (Low Temperature Discharge)
Instructions 1. Set Default state at 15
2. Resting at -35°C (at or below -33.2°C)
3. Discharging at -100 mA, -35°C (at or below -33.2°C)
4. Discharging at -100 mA, -30°C (at or above -32.2°C)
5. Charging at 100 mA, -25°C (at or below -23.2°C)
6. Charging at 100 mA, -20°C (at or above -22.2°C)
Pass / Fail Criteria ⦁ Prefault overtemp Discharge value is False (default state)
⦁ Fault overtemp Discharge value is False (default state)
⦁ Prefault overtemp charge value is False (default state)
⦁ Fault overtemp charge value is False (default state)
⦁ Permanent Disable overtemp is False (default state)
⦁ Measure output fets disabled is False (default state)
⦁ Resting overtemp discharge is True for fault & prefault
⦁ Discharging overtemp value is True for fault & prefault
⦁ After charging permanent disable, permanent disable overtemp is True
⦁ After resting permanent disable, permanent disable overtemp is True
Estimated Duration 10 seconds
Note This occurs when we read more than 20 mamps from the battery, if any of the cells are under 0 degrees Celsius this will trigger a fault. This will be cleared if we go above -2C. Regardless of current being measured, if we ever read below -20C, this should trigger a fault. This fault should not be cleared until we are above -18C
def set_exact_volts( self, cell: hitl_tester.modules.bms.cell.Cell, voltage: float, compensation: float = 0.08):
2789    def set_exact_volts(self, cell: Cell, voltage: float, compensation: float = 0.08):
2790        """What the BMS reads won't exactly match the set voltage, thus we need slight adjustments."""
2791        cell.exact_volts = voltage + compensation
2792        logger.write_debug_to_report(f"Cell is {cell.volts}V")

What the BMS reads won't exactly match the set voltage, thus we need slight adjustments.

def test_overvoltage_faults( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
2794    def test_overvoltage_faults(self, serial_watcher: SerialWatcher):
2795        """
2796        | Description          | Test over voltage faults                                             |
2797        | :------------------- | :--------------------------------------------------------------------- |
2798        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2799        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2800jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2801        | MIL-PRF Sections     | 4.6.1 (Standard Charge)                                                |
2802        | Instructions         | 1. Rest at 0 mA, 3.8002 V                                         </br>\
2803                                 2. Charge at 100 mA, 4.21 V                                       </br>\
2804                                 3. Charge at 100 mA, 4.10 V                                       </br>\
2805                                 4. Charge at 100 mA, 4.26 V                                       </br>\
2806                                 5. Charge at 100 mA, 4.10 V                                            |
2807        | Pass / Fail Criteria | ⦁ Prefault over voltage charge is False at 3.8002 V               </br>\
2808                                 ⦁ Fault over voltage charge is False at 3.8002 V                  </br>\
2809                                 ⦁ Permanent disable over votlage charge is False at 3.8002 V      </br>\
2810                                 ⦁ Prefault over voltage charge is True at 4.21 V                  </br>\
2811                                 ⦁ Fault over voltage charge is True at 4.21 V                     </br>\
2812                                 ⦁ Prefault over voltage charge is False at 4.10 V                 </br>\
2813                                 ⦁ Fault over voltage charge is False at 4.10 V                    </br>\
2814                                 ⦁ Permanent disable over voltage is True at 4.26 V                </br>\
2815                                 ⦁ Permanent disable over voltage is False at 4.10 V                    |
2816        | Estimated Duration   | 10 seconds                                                             |
2817        | Note                 | While charging, we need to monitor the voltage of our cells.           \
2818                                 Specifically, if a cell ever goes above 4.205 Volts, we should         \
2819                                 trigger a prefault. If this prefault exsists for more than 3 seconds,  \
2820                                 we then should trigger a full fault. If a cell ever gets to be above   \
2821                                 4.250 volts, we should trigger a permanent fault. If we go under 4.201 \
2822                                 we should be able to clear the fault                                   |
2823        """
2824        logger.write_info_to_report("Testing Overvoltage")
2825        assert _bms.load and _bms.charger  # Confirm hardware is available
2826        test_cell = _bms.cells[1]
2827        for cell in _bms.cells.values():
2828            cell.disengage_safety_protocols = True
2829            # Must be high enough to not trigger cell imbalance [abs(high - low) > 0.5V]
2830            self.set_exact_volts(cell, 4.0, 0)
2831
2832        logger.write_result_to_html_report(f"Resting at 0 mA, {CELL_VOLTAGE} V")
2833        serial_watcher.assert_true("flags.prefault_overvoltage_charge", False, 1)
2834        serial_watcher.assert_true("flags.fault_overvoltage_charge", False, 1)
2835        serial_watcher.assert_true("flags.permanentdisable_overvoltage", False, 1)
2836
2837        with _bms.charger(16.8, 0.200):
2838            logger.write_result_to_html_report("Charging at 100 mA, 4.205+ V")
2839            self.set_exact_volts(test_cell, 4.22, 0)
2840            serial_watcher.assert_true("flags.prefault_overvoltage_charge", True, 2)
2841            serial_watcher.assert_true("flags.fault_overvoltage_charge", True, 2)
2842
2843            logger.write_result_to_html_report("Charging at 100 mA, 4.10 V")
2844            self.set_exact_volts(test_cell, 4.10, 0)
2845            serial_watcher.assert_true("flags.prefault_overvoltage_charge", False, 3)
2846            serial_watcher.assert_true("flags.fault_overvoltage_charge", False, 3)
2847
2848            logger.write_result_to_html_report("Charging at 100 mA, 4.25+ V")
2849            self.set_exact_volts(test_cell, 4.27, 0)
2850            serial_watcher.assert_true("flags.permanentdisable_overvoltage", True, 2)
2851
2852            logger.write_result_to_html_report("Charging at 100 mA, 4.10 V")
2853            self.set_exact_volts(test_cell, 4.10, 0)
2854            serial_watcher.assert_false("flags.permanentdisable_overvoltage", False, 3)
Description Test over voltage faults
GitHub Issue turnaroundfactor/HITL#476
Google Docs Google Sheet Cell
MIL-PRF Sections 4.6.1 (Standard Charge)
Instructions 1. Rest at 0 mA, 3.8002 V
2. Charge at 100 mA, 4.21 V
3. Charge at 100 mA, 4.10 V
4. Charge at 100 mA, 4.26 V
5. Charge at 100 mA, 4.10 V
Pass / Fail Criteria ⦁ Prefault over voltage charge is False at 3.8002 V
⦁ Fault over voltage charge is False at 3.8002 V
⦁ Permanent disable over votlage charge is False at 3.8002 V
⦁ Prefault over voltage charge is True at 4.21 V
⦁ Fault over voltage charge is True at 4.21 V
⦁ Prefault over voltage charge is False at 4.10 V
⦁ Fault over voltage charge is False at 4.10 V
⦁ Permanent disable over voltage is True at 4.26 V
⦁ Permanent disable over voltage is False at 4.10 V
Estimated Duration 10 seconds
Note While charging, we need to monitor the voltage of our cells. Specifically, if a cell ever goes above 4.205 Volts, we should trigger a prefault. If this prefault exsists for more than 3 seconds, we then should trigger a full fault. If a cell ever gets to be above 4.250 volts, we should trigger a permanent fault. If we go under 4.201 we should be able to clear the fault
def test_undervoltage_faults( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
2856    def test_undervoltage_faults(self, serial_watcher: SerialWatcher):
2857        """
2858        | Description          | Test under voltage faults                                             |
2859        | :------------------- | :--------------------------------------------------------------------- |
2860        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2861        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2862jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2863        | MIL-PRF Sections     | 4.6.1 (Standard Charge)                                                |
2864        | Instructions         | 1. Rest at 0 mA, 3.0 V                                            </br>\
2865                                 2. Rest at 0 mA, 2.3 V                                            </br>\
2866                                 3. Rest at 0 mA, 2.6 V                                            </br>\
2867                                 4. Charge at 500 mA, 2.3 V                                        </br>\
2868                                 5. Charge at 500 mA, 2.4 V                                        </br>\
2869                                 6. Charge at 500 mA, 2.2 V                                        </br>\
2870                                 5. Charge at 500 mA, 2.6 V                                             |
2871        | Pass / Fail Criteria | ⦁ Prefault under voltage discharge is False at 0mA, 3.0 V         </br>\
2872                                 ⦁ Fault slumber under voltage discharge is False at 0mA, 3.0 V    </br>\
2873                                 ⦁ Prefault under voltage charge is False at 0mA, 3.0 V            </br>\
2874                                 ⦁ Permanent disable under voltage is False at 0mA, 3.0 V          </br>\
2875                                 ⦁ Prefault under voltage discharge is True when resting at 0mA, 2.3 V  </br>\
2876                                 ⦁ Fault slumber under voltage is True when resting at 0mA, 2.3 V  </br>\
2877                                 ⦁ Prefault under voltage discharge is False when resting at 0mA, 2.6 V </br>\
2878                                 ⦁ Fault slumber under voltage is False when resting at 0mA, 2.6 V </br>\
2879                                 ⦁ Prefault under voltage charge is True when charging at 500mA, 2.3 V  </br>\
2880                                 ⦁ Fault slumber under voltage is True when charging at 500mA, 2.3 V    </br>\
2881                                 ⦁ Prefault under voltage discharge is False when charging at 500mA, 2.4 V  </br>\
2882                                 ⦁ Fault slumber under voltage is False when charging at 500mA, 2.4 V   </br>\
2883                                 ⦁ Permanent disable under voltage is True when charging at 500mA, 2.2 V    </br>\
2884                                 ⦁ Permanent disable under voltage is False when charging at 500mA, 2.6 V   |
2885        | Estimated Duration   | 10 seconds                                                             |
2886        | Note                 | This has also been validated in software, meaning the logic should     \
2887                                 properly handle a situation with a cell discharging too low, however   \
2888                                 this has not yet been tested in hardware with a cell sensor reading    \
2889                                 that low of voltage and triggering a fault. If we are reading less     \
2890                                 than 20mamps from the cells, we should be able to trigger an           \
2891                                 under-voltage fault. If we read less than 2.4 volts, we must           \
2892                                 trigger a fault. If this fault persists for over 1 second, we          \
2893                                 should then trigger a full fault. We will not clear this fault         \
2894                                 unless we are able to read above 2.5 volts. If we are reading over     \
2895                                 20 mamps and a cell reads less than 2.325 volts, we must trigger a     \
2896                                 cell voltage charge min prefault, if this persists for another bms     \
2897                                 software cycle we will trigger a full fault. This fault will clear     \
2898                                 when we read above this voltage. If the cell voltage ever goes under   \
2899                                 2.3 while charging, we must trigger a permanent fault.                 |
2900        """
2901        logger.write_info_to_report("Testing Undervoltage")
2902        assert _bms.load and _bms.charger  # Confirm hardware is available
2903        test_cell = _bms.cells[1]
2904        for cell in _bms.cells.values():
2905            cell.disengage_safety_protocols = True
2906            # Must be low enough to not trigger cell imbalance [abs(high - low) > 0.5V]
2907            self.set_exact_volts(cell, 2.6, 0.00)
2908
2909        logger.write_result_to_html_report("Resting at 0 mA, 3.0 V")
2910        serial_watcher.assert_true("flags.prefault_undervoltage_discharge", False, 1)
2911        serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False, 1)
2912        serial_watcher.assert_true("flags.prefault_undervoltage_charge", False, 1)
2913        serial_watcher.assert_true("flags.fault_undervoltage_charge", False, 1)
2914        serial_watcher.assert_true("flags.permanentdisable_undervoltage", False, 1)
2915
2916        with _bms.load(0.500):
2917            logger.write_result_to_html_report("Discharging at -500 mA, 2.325 V")
2918            self.set_exact_volts(test_cell, 2.320, 0.00)
2919            serial_watcher.assert_true("flags.prefault_undervoltage_discharge", True, 2, wait_time=600)
2920            serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", True, 2, wait_time=600)
2921
2922            logger.write_result_to_html_report("Discharging at -500 mA, 2.6 V")
2923            self.set_exact_volts(test_cell, 2.6, 0.00)
2924            serial_watcher.assert_true("flags.prefault_undervoltage_discharge", False, 3, wait_time=600)
2925            serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False, 3, wait_time=600)
2926
2927        with _bms.charger(16.8, 0.500):
2928            logger.write_result_to_html_report("Charging at 500 mA, 2.325 V")
2929            self.set_exact_volts(test_cell, 2.320, 0.00)
2930            serial_watcher.assert_true("flags.prefault_undervoltage_charge", True, 2, wait_time=600)
2931            serial_watcher.assert_true("flags.fault_undervoltage_charge", True, 2, wait_time=600)
2932
2933            logger.write_result_to_html_report("Charging at 500 mA, 2.4 V")
2934            self.set_exact_volts(test_cell, 2.6, 0.00)
2935            serial_watcher.assert_true("flags.prefault_undervoltage_charge", False, 3)
2936            serial_watcher.assert_true("flags.fault_undervoltage_charge", False, 3)
2937
2938            logger.write_result_to_html_report("Charging at 500 mA, 2.2 V")
2939            self.set_exact_volts(test_cell, 2.2, 0.00)
2940            serial_watcher.assert_true("flags.permanentdisable_undervoltage", True, 2)
2941
2942            logger.write_result_to_html_report("Charging at 500 mA, 2.6 V")
2943            self.set_exact_volts(test_cell, 2.6, 0.00)
2944            serial_watcher.assert_false("flags.permanentdisable_undervoltage", False, 3)
Description Test under voltage faults
GitHub Issue turnaroundfactor/HITL#476
Google Docs Google Sheet Cell
MIL-PRF Sections 4.6.1 (Standard Charge)
Instructions 1. Rest at 0 mA, 3.0 V
2. Rest at 0 mA, 2.3 V
3. Rest at 0 mA, 2.6 V
4. Charge at 500 mA, 2.3 V
5. Charge at 500 mA, 2.4 V
6. Charge at 500 mA, 2.2 V
5. Charge at 500 mA, 2.6 V
Pass / Fail Criteria ⦁ Prefault under voltage discharge is False at 0mA, 3.0 V
⦁ Fault slumber under voltage discharge is False at 0mA, 3.0 V
⦁ Prefault under voltage charge is False at 0mA, 3.0 V
⦁ Permanent disable under voltage is False at 0mA, 3.0 V
⦁ Prefault under voltage discharge is True when resting at 0mA, 2.3 V
⦁ Fault slumber under voltage is True when resting at 0mA, 2.3 V
⦁ Prefault under voltage discharge is False when resting at 0mA, 2.6 V
⦁ Fault slumber under voltage is False when resting at 0mA, 2.6 V
⦁ Prefault under voltage charge is True when charging at 500mA, 2.3 V
⦁ Fault slumber under voltage is True when charging at 500mA, 2.3 V
⦁ Prefault under voltage discharge is False when charging at 500mA, 2.4 V
⦁ Fault slumber under voltage is False when charging at 500mA, 2.4 V
⦁ Permanent disable under voltage is True when charging at 500mA, 2.2 V
⦁ Permanent disable under voltage is False when charging at 500mA, 2.6 V
Estimated Duration 10 seconds
Note This has also been validated in software, meaning the logic should properly handle a situation with a cell discharging too low, however this has not yet been tested in hardware with a cell sensor reading that low of voltage and triggering a fault. If we are reading less than 20mamps from the cells, we should be able to trigger an under-voltage fault. If we read less than 2.4 volts, we must trigger a fault. If this fault persists for over 1 second, we should then trigger a full fault. We will not clear this fault unless we are able to read above 2.5 volts. If we are reading over 20 mamps and a cell reads less than 2.325 volts, we must trigger a cell voltage charge min prefault, if this persists for another bms software cycle we will trigger a full fault. This fault will clear when we read above this voltage. If the cell voltage ever goes under 2.3 while charging, we must trigger a permanent fault.
def test_cell_imbalance( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
2946    def test_cell_imbalance(self, serial_watcher: SerialWatcher):
2947        """
2948        | Description          | Test Cell Imbalance                                                    |
2949        | :------------------- | :--------------------------------------------------------------------- |
2950        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2951        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2952jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2953        | MIL-PRF Sections     | 4.7.2.1 (Cell balance)                                                 |
2954        | Instructions         | 1. Rest at 0 mA, 3.8 V                                            </br>\
2955                                 2. Rest at 0 mA, 2.0 V                                            </br>\
2956                                 3. Rest at 0 mA, 3.8 V                                                 |
2957        | Pass / Fail Criteria | ⦁ Prefault cell imbalance is False after resting at 0mA, 3.8 V    </br>\
2958                                 ⦁ Permanent disable cell imbalance is False after resting at 0mA, 3.8 V    </br>\
2959                                 ⦁ Prefault cell imbalance is True after resting at 0mA, 2.0 V     </br>\
2960                                 ⦁ Permanent disable cell imbalance is True after resting at 0mA, 2.0 V    </br>\
2961                                 ⦁ Permanent disable cell imbalance is False after resting at 0mA, 3.8 V |
2962        | Estimated Duration   | 10 seconds                                                             |
2963        | Note                 | Occurs when the difference between the highest and lowest cell is 0.5V.|
2964        """
2965        logger.write_info_to_report("Testing Cell Imbalance")
2966        assert _bms.load and _bms.charger  # Confirm hardware is available
2967        test_cell = _bms.cells[1]
2968        test_cell.disengage_safety_protocols = True
2969
2970        # Set all cell voltages
2971        for cell in _bms.cells.values():
2972            cell.volts = CELL_VOLTAGE
2973
2974        voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
2975        logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
2976        serial_watcher.assert_true("flags.permanentdisable_cellimbalance", False, 1)
2977
2978        self.set_exact_volts(test_cell, 2.0)
2979        voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
2980        logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
2981        serial_watcher.assert_true("flags.permanentdisable_cellimbalance", True, 2)
2982
2983        self.set_exact_volts(test_cell, CELL_VOLTAGE)
2984        voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
2985        logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
2986        serial_watcher.assert_false("flags.permanentdisable_cellimbalance", False, 3)
Description Test Cell Imbalance
GitHub Issue turnaroundfactor/HITL#476
Google Docs Google Sheet Cell
MIL-PRF Sections 4.7.2.1 (Cell balance)
Instructions 1. Rest at 0 mA, 3.8 V
2. Rest at 0 mA, 2.0 V
3. Rest at 0 mA, 3.8 V
Pass / Fail Criteria ⦁ Prefault cell imbalance is False after resting at 0mA, 3.8 V
⦁ Permanent disable cell imbalance is False after resting at 0mA, 3.8 V
⦁ Prefault cell imbalance is True after resting at 0mA, 2.0 V
⦁ Permanent disable cell imbalance is True after resting at 0mA, 2.0 V
⦁ Permanent disable cell imbalance is False after resting at 0mA, 3.8 V
Estimated Duration 10 seconds
Note Occurs when the difference between the highest and lowest cell is 0.5V.
def test_overvoltage_overtemp_faults( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
2988    def test_overvoltage_overtemp_faults(self, serial_watcher: SerialWatcher):
2989        """
2990        | Description          | Test combined high temperature & high voltage                          |
2991        | :------------------- | :--------------------------------------------------------------------- |
2992        | GitHub Issue         | turnaroundfactor/HITL#476                                       |
2993        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
2994jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
2995        | MIL-PRF Sections     | 4.7.3.3 (Extreme high temperature discharge)                           |
2996        | Instructions         | 1. Rest at 0 mA, 3.8002 V, 15°C                                   </br>\
2997                                 2. Charge at 100 mA, 4.21 V, 54°C                                 </br>\
2998                                 3. Charge at 100 mA, 4.10 V, 15°C                                 </br>\
2999                                 4. Charge at 100 mA, 4.26 V, 94°C                                 </br>\
3000                                 5. Charge at 100 mA, 4.10 V, 15°C                                      |
3001        | Pass / Fail Criteria | ⦁ Prefault over voltage charge is False at 0 mA, 3.8002 V, 15°C   </br>\
3002                                 ⦁ Fault over voltage charge is False at 0 mA, 3.8002 V, 15°C      </br>\
3003                                 ⦁ Permanent disable over votlage charge is False at 0 mA, 3.8002 V, 15°C </br>\
3004                                 ⦁ Prefault over voltage charge is True at 4.21 V, 54°C            </br>\
3005                                 ⦁ Fault over voltage charge is True at 4.21 V, 54°C               </br>\
3006                                 ⦁ Prefault over temperature charge is True at 4.21 V, 54°C        </br>\
3007                                 ⦁ Fault over temperature charge is True at 4.21 V, 54°C           </br>\
3008                                 ⦁ Prefault over voltage charge is False at 4.10 V, 15°C           </br>\
3009                                 ⦁ Prefault over temperature charge is False at 4.10 V, 15°C       </br>\
3010                                 ⦁ Fault over voltage charge is False at 4.10 V, 15°C              </br>\
3011                                 ⦁ Fault over temperature charge is False at 4.10 V                </br>\
3012                                 ⦁ Permanent disable over voltage is True at 4.26 V, 94°C          </br>\
3013                                 ⦁ Permanent disable over temperature is True at 4.26 V, 94°C      </br>\
3014                                 ⦁ Permanent disable over voltage is False at  4.10 V, 15°C        </br>\
3015                                 ⦁ Permanent disable over temperature is False at  4.10 V, 15°C         |
3016        | Estimated Duration   | 10 seconds                                                             |
3017        | Note                 | Combine over voltage and over temperature faults                       |
3018        """
3019        logger.write_info_to_report("Testing Overvoltage with Overtemp")
3020        assert _bms.load and _bms.charger  # Confirm hardware is available
3021        test_cell = _bms.cells[1]
3022        for cell in _bms.cells.values():
3023            cell.disengage_safety_protocols = True
3024            # Must be high enough to not trigger cell imbalance [abs(high - low) > 0.5V]
3025            self.set_exact_volts(cell, 4.0, 0.0)
3026
3027        logger.write_result_to_html_report(f"Resting at 0 mA, {CELL_VOLTAGE} V, 15°C")
3028        serial_watcher.assert_true("flags.prefault_overvoltage_charge", False, 1)
3029        serial_watcher.assert_true("flags.fault_overvoltage_charge", False, 1)
3030        serial_watcher.assert_true("flags.permanentdisable_overvoltage", False, 1)
3031
3032        serial_watcher.assert_true("flags.prefault_overtemp_charge", False, 1)
3033        serial_watcher.assert_true("flags.fault_overtemp_charge", False, 1)
3034        serial_watcher.assert_true("flags.permanentdisable_overtemp", False, 1)
3035
3036        with _bms.charger(16.8, 0.200):
3037            logger.write_result_to_html_report("Charging at 100 mA, 4.21 V, 54°C")
3038            self.set_exact_volts(test_cell, 4.22, 0.0)
3039            _plateset.thermistor1 = 54
3040            serial_watcher.assert_true("flags.prefault_overvoltage_charge", True, 2)
3041            serial_watcher.assert_true("flags.fault_overvoltage_charge", True, 2)
3042            serial_watcher.assert_true("flags.prefault_overtemp_charge", True, 2)
3043            serial_watcher.assert_true("flags.fault_overtemp_charge", True, 2)
3044
3045            logger.write_result_to_html_report("Charging at 100 mA, 4.10 V, 15°C")
3046            self.set_exact_volts(test_cell, 4.10, 0.0)
3047            _plateset.thermistor1 = 15
3048            serial_watcher.assert_true("flags.prefault_overvoltage_charge", False, 3)
3049            serial_watcher.assert_true("flags.fault_overvoltage_charge", False, 3)
3050            serial_watcher.assert_true("flags.prefault_overtemp_charge", False, 3)
3051            serial_watcher.assert_true("flags.fault_overtemp_charge", False, 3)
Description Test combined high temperature & high voltage
GitHub Issue turnaroundfactor/HITL#476
Google Docs Google Sheet Cell
MIL-PRF Sections 4.7.3.3 (Extreme high temperature discharge)
Instructions 1. Rest at 0 mA, 3.8002 V, 15°C
2. Charge at 100 mA, 4.21 V, 54°C
3. Charge at 100 mA, 4.10 V, 15°C
4. Charge at 100 mA, 4.26 V, 94°C
5. Charge at 100 mA, 4.10 V, 15°C
Pass / Fail Criteria ⦁ Prefault over voltage charge is False at 0 mA, 3.8002 V, 15°C
⦁ Fault over voltage charge is False at 0 mA, 3.8002 V, 15°C
⦁ Permanent disable over votlage charge is False at 0 mA, 3.8002 V, 15°C
⦁ Prefault over voltage charge is True at 4.21 V, 54°C
⦁ Fault over voltage charge is True at 4.21 V, 54°C
⦁ Prefault over temperature charge is True at 4.21 V, 54°C
⦁ Fault over temperature charge is True at 4.21 V, 54°C
⦁ Prefault over voltage charge is False at 4.10 V, 15°C
⦁ Prefault over temperature charge is False at 4.10 V, 15°C
⦁ Fault over voltage charge is False at 4.10 V, 15°C
⦁ Fault over temperature charge is False at 4.10 V
⦁ Permanent disable over voltage is True at 4.26 V, 94°C
⦁ Permanent disable over temperature is True at 4.26 V, 94°C
⦁ Permanent disable over voltage is False at 4.10 V, 15°C
⦁ Permanent disable over temperature is False at 4.10 V, 15°C
Estimated Duration 10 seconds
Note Combine over voltage and over temperature faults
def test_undervoltage_undertemp_faults( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
3053    def test_undervoltage_undertemp_faults(self, serial_watcher: SerialWatcher):
3054        """
3055        | Description          | Test combined low temperature & low voltage                            |
3056        | :------------------- | :--------------------------------------------------------------------- |
3057        | GitHub Issue         | turnaroundfactor/HITL#476                                              |
3058        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3059jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
3060        | MIL-PRF Sections     | 4.7.3.2 (Extreme low temperature Discharge)                            |
3061        | Instructions         | 1. Rest at 0 mA, 3.0 V, 15°C                                      </br>\
3062                                 2. Rest at 0 mA, 2.3 V, -21°C                                     </br>\
3063                                 3. Rest at 0 mA, 2.6 V, -17°C                                     </br>\
3064                                 4. Charge at 500 mA, 2.3 V, -25°C                                  </br>\
3065                                 5. Charge at 500 mA, 2.4 V, -20°C                                  </br>\
3066                                 6. Charge at 500 mA, 2.2 V,  15°C                                 </br>\
3067                                 7. Charge at 500 mA, 2.6 V,  15°C                                      |
3068        | Pass / Fail Criteria | ⦁ Prefault under voltage discharge is False at 0 mA, 3.0 V, 15°C  </br>\
3069                                 ⦁ Fault slumber under voltage charge is False at 0 mA, 3.0 V, 15°C</br>\
3070                                 ⦁ Prefault under voltage charge is False at 0 mA, 3.0 V, 15°C     </br>\
3071                                 ⦁ Fault under voltage charge is False at 0 mA, 3.0 V, 15°C        </br>\
3072                                 ⦁ Permanent disable under voltage is False at 0 mA, 3.0 V, 15°C   </br>\
3073                                 ⦁ Prefault under temperature discharge is False at 0 mA, 3.0 V, 15°C </br>\
3074                                 ⦁ Fault under temperature discharge is False at 0 mA, 3.0 V, 15°C </br>\
3075                                 ⦁ Prefault under temperature charge is False at 0 mA, 3.0 V, 15°C </br>\
3076                                 ⦁ Fault under temperature charge is False at 0 mA, 3.0 V, 15°C    </br>\
3077                                 ⦁ Fault under voltage charge is False at 0 mA, 3.0 V, 15°C        </br>\
3078                                 ⦁ Fault under voltage charge is False at 0 mA, 3.0 V, 15°C        </br>\
3079                                 ⦁ Prefault under voltage discharge is True at 0 mA, 2.3 V, -21°C  </br>\
3080                                 ⦁ Fault slumber under voltage discharge is True at 0 mA, 2.3 V, -21°C </br>\
3081                                 ⦁ Prefault under temperature discharge is True at 0 mA, 2.3 V, -21°C  </br>\
3082                                 ⦁ Fault under temperature discharge is True at 0 mA, 2.3 V, -21°C </br>\
3083                                 ⦁ Prefault under voltage discharge is False at 0 mA, 2.6 V, -17°C </br>\
3084                                 ⦁ Fault slumber under voltage discharge is False at 0 mA, 2.6 V, -17°C </br>\
3085                                 ⦁ Prefault under temperature discharge is False at 0 mA, 2.6 V, -17°C  </br>\
3086                                 ⦁ Fault under temperature discharge is False at 0 mA, 2.6 V, -17°C</br>\
3087                                 ⦁ Prefault under voltage charge is True at 500 mA, 2.3 V, -25°C    </br>\
3088                                 ⦁ Fault slumber under voltage charge is True at 500 mA, 2.3 V, -25°C</br>\
3089                                 ⦁ Prefault under temperature charge is True at 500 mA, 2.3 V, -25°C</br>\
3090                                 ⦁ Fault under temperature charge is True at 500 mA, 2.3 V, -25°C   </br>\
3091                                 ⦁ Prefault under voltage charge is False at 500 mA, 2.4 V, -20°C    </br>\
3092                                 ⦁ Fault slumber under voltage charge is False at 500 mA, 2.4 V, -20°C </br>\
3093                                 ⦁ Prefault under temperature charge is False at 500 mA, 2.4 V, -20°C</br>\
3094                                 ⦁ Fault under temperature charge is False at 500 mA, 2.4 V, -20°C   </br>\
3095                                 ⦁ Permanent disable under voltage is True at 500 mA, 2.2 V, 15°C  </br>\
3096                                 ⦁ Permanent disable under voltage is False at 500 mA, 2.6 V, 15°C      |
3097        | Estimated Duration   | 10 seconds                                                             |
3098        | Note                 | Combine under voltage and under temperature faults                     |
3099        """
3100
3101        logger.write_info_to_report("Testing Undervoltage with Undertemp")
3102        assert _bms.load and _bms.charger  # Confirm hardware is available
3103        test_cell = _bms.cells[1]
3104        for cell in _bms.cells.values():
3105            cell.disengage_safety_protocols = True
3106            self.set_exact_volts(
3107                cell, 2.8, 0
3108            )  # Must be low enough to not trigger cell imbalance [abs(high - low) > 0.5V]
3109
3110        logger.write_result_to_html_report("Resting at 0 mA, 3.0 V, 15°C")
3111        _plateset.thermistor1 = 15
3112        serial_watcher.assert_true("flags.prefault_undervoltage_discharge", False, 1)
3113        serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False, 1)
3114        serial_watcher.assert_true("flags.prefault_undervoltage_charge", False, 1)
3115        serial_watcher.assert_true("flags.fault_undervoltage_charge", False, 1)
3116        serial_watcher.assert_true("flags.permanentdisable_undervoltage", False, 1)
3117
3118        serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 1)
3119        serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 1)
3120        serial_watcher.assert_true("flags.prefault_undertemp_charge", False, 1)
3121        serial_watcher.assert_true("flags.fault_undertemp_charge", False, 1)
3122
3123        with _bms.load(0.500):
3124            logger.write_result_to_html_report("Discharging at -500 mA, 2.325 V, -35°C")
3125            self.set_exact_volts(test_cell, 2.320, 0)
3126            _plateset.thermistor1 = -35
3127            serial_watcher.assert_true("flags.prefault_undervoltage_discharge", True, 2)
3128            serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", True, 2)
3129            serial_watcher.assert_true("flags.prefault_undertemp_discharge", True, 2)
3130            serial_watcher.assert_true("flags.fault_undertemp_discharge", True, 2)
3131
3132            logger.write_result_to_html_report("Discharging at -500 mA, 2.6 V, -30°C")
3133            self.set_exact_volts(test_cell, 2.8, 0)
3134            _plateset.thermistor1 = -30
3135            serial_watcher.assert_true("flags.prefault_undervoltage_discharge", False, 3)
3136            serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False, 3)
3137            serial_watcher.assert_true("flags.prefault_undertemp_discharge", False, 3)
3138            serial_watcher.assert_true("flags.fault_undertemp_discharge", False, 3)
3139
3140        with _bms.charger(16.8, 0.500):
3141            logger.write_result_to_html_report("Charging at 500 mA, 2.325 V, -25°C")
3142            self.set_exact_volts(test_cell, 2.320, 0.00)
3143            _plateset.thermistor1 = -25
3144            serial_watcher.assert_true("flags.prefault_undervoltage_charge", True, 2)
3145            serial_watcher.assert_true("flags.fault_undervoltage_charge", True, 2)
3146            serial_watcher.assert_true("flags.prefault_undertemp_charge", True, 2)
3147            serial_watcher.assert_true("flags.fault_undertemp_charge", True, 2)
3148
3149            logger.write_result_to_html_report("Charging at 500 mA, 2.6 V, -20°C")
3150            self.set_exact_volts(test_cell, 2.8, 0)
3151            _plateset.thermistor1 = -20
3152            serial_watcher.assert_true("flags.prefault_undervoltage_charge", False, 3)
3153            serial_watcher.assert_true("flags.fault_undervoltage_charge", False, 3)
3154            serial_watcher.assert_true("flags.prefault_undertemp_charge", False, 3)
3155            serial_watcher.assert_true("flags.fault_undertemp_charge", False, 3)
Description Test combined low temperature & low voltage
GitHub Issue turnaroundfactor/HITL#476
Google Docs Google Sheet Cell
MIL-PRF Sections 4.7.3.2 (Extreme low temperature Discharge)
Instructions 1. Rest at 0 mA, 3.0 V, 15°C
2. Rest at 0 mA, 2.3 V, -21°C
3. Rest at 0 mA, 2.6 V, -17°C
4. Charge at 500 mA, 2.3 V, -25°C
5. Charge at 500 mA, 2.4 V, -20°C
6. Charge at 500 mA, 2.2 V, 15°C
7. Charge at 500 mA, 2.6 V, 15°C
Pass / Fail Criteria ⦁ Prefault under voltage discharge is False at 0 mA, 3.0 V, 15°C
⦁ Fault slumber under voltage charge is False at 0 mA, 3.0 V, 15°C
⦁ Prefault under voltage charge is False at 0 mA, 3.0 V, 15°C
⦁ Fault under voltage charge is False at 0 mA, 3.0 V, 15°C
⦁ Permanent disable under voltage is False at 0 mA, 3.0 V, 15°C
⦁ Prefault under temperature discharge is False at 0 mA, 3.0 V, 15°C
⦁ Fault under temperature discharge is False at 0 mA, 3.0 V, 15°C
⦁ Prefault under temperature charge is False at 0 mA, 3.0 V, 15°C
⦁ Fault under temperature charge is False at 0 mA, 3.0 V, 15°C
⦁ Fault under voltage charge is False at 0 mA, 3.0 V, 15°C
⦁ Fault under voltage charge is False at 0 mA, 3.0 V, 15°C
⦁ Prefault under voltage discharge is True at 0 mA, 2.3 V, -21°C
⦁ Fault slumber under voltage discharge is True at 0 mA, 2.3 V, -21°C
⦁ Prefault under temperature discharge is True at 0 mA, 2.3 V, -21°C
⦁ Fault under temperature discharge is True at 0 mA, 2.3 V, -21°C
⦁ Prefault under voltage discharge is False at 0 mA, 2.6 V, -17°C
⦁ Fault slumber under voltage discharge is False at 0 mA, 2.6 V, -17°C
⦁ Prefault under temperature discharge is False at 0 mA, 2.6 V, -17°C
⦁ Fault under temperature discharge is False at 0 mA, 2.6 V, -17°C
⦁ Prefault under voltage charge is True at 500 mA, 2.3 V, -25°C
⦁ Fault slumber under voltage charge is True at 500 mA, 2.3 V, -25°C
⦁ Prefault under temperature charge is True at 500 mA, 2.3 V, -25°C
⦁ Fault under temperature charge is True at 500 mA, 2.3 V, -25°C
⦁ Prefault under voltage charge is False at 500 mA, 2.4 V, -20°C
⦁ Fault slumber under voltage charge is False at 500 mA, 2.4 V, -20°C
⦁ Prefault under temperature charge is False at 500 mA, 2.4 V, -20°C
⦁ Fault under temperature charge is False at 500 mA, 2.4 V, -20°C
⦁ Permanent disable under voltage is True at 500 mA, 2.2 V, 15°C
⦁ Permanent disable under voltage is False at 500 mA, 2.6 V, 15°C
Estimated Duration 10 seconds
Note Combine under voltage and under temperature faults
def test_cell_imbalance_charge( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
3157    def test_cell_imbalance_charge(self, serial_watcher: SerialWatcher):
3158        """
3159        | Description          | Test Cell Imbalance                                                    |
3160        | :------------------- | :--------------------------------------------------------------------- |
3161        | GitHub Issue         | turnaroundfactor/HITL#476                                              |
3162        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3163jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D6) |
3164        | MIL-PRF Sections     | 4.7.2.1 (Cell balance)                                                 |
3165        | Instructions         | 1. Rest at 0 mA                                                   </br>\
3166                                 2. Charge at 1 mA                                                 </br>\
3167                                 3. Rest at 0 mA                                                        |
3168        | Pass / Fail Criteria | ⦁ Prefault cell imbalance is False after resting at 0mA           </br>\
3169                                 ⦁ Permanent disable cell imbalance is False after resting at 0mA  </br>\
3170                                 ⦁ Prefault cell imbalance is True after resting at 1 A            </br>\
3171                                 ⦁ Permanent disable cell imbalance is True after resting 1 A      </br>\
3172                                 ⦁ Prefault cell imbalance is True after resting at 0 A            </br>\
3173                                 ⦁ Permanent disable cell imbalance is False after resting at 0 A       |
3174        | Estimated Duration   | 10 seconds                                                             |
3175        | Note                 | Occurs when the difference between the highest and lowest cell is 0.5V.|
3176        """
3177        logger.write_info_to_report("Testing Cell Imbalance Charge")
3178        assert _bms.load and _bms.charger  # Confirm hardware is available
3179
3180        test_cell = _bms.cells[1]
3181        test_cell.disengage_safety_protocols = True
3182        for cell in _bms.cells.values():
3183            cell.exact_volts = 4.2
3184
3185        voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
3186        logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
3187        serial_watcher.assert_true("flags.permanentdisable_cellimbalance", False, 1)
3188
3189        with _bms.charger(16.8, 1):
3190            self.set_exact_volts(test_cell, 2.5)
3191            voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
3192            logger.write_result_to_html_report(f"Charging at 1 A, {', '.join(voltages)}")
3193            serial_watcher.assert_true("flags.permanentdisable_cellimbalance", True)
3194
3195            test_cell.exact_volts = 4.2
3196            voltages = [f"{cell.measured_volts} V" for cell in _bms.cells.values()]
3197            logger.write_result_to_html_report(f"Resting at 0 mA, {', '.join(voltages)}")
3198            serial_watcher.assert_false("flags.permanentdisable_cellimbalance", True)
Description Test Cell Imbalance
GitHub Issue turnaroundfactor/HITL#476
Google Docs Google Sheet Cell
MIL-PRF Sections 4.7.2.1 (Cell balance)
Instructions 1. Rest at 0 mA
2. Charge at 1 mA
3. Rest at 0 mA
Pass / Fail Criteria ⦁ Prefault cell imbalance is False after resting at 0mA
⦁ Permanent disable cell imbalance is False after resting at 0mA
⦁ Prefault cell imbalance is True after resting at 1 A
⦁ Permanent disable cell imbalance is True after resting 1 A
⦁ Prefault cell imbalance is True after resting at 0 A
⦁ Permanent disable cell imbalance is False after resting at 0 A
Estimated Duration 10 seconds
Note Occurs when the difference between the highest and lowest cell is 0.5V.
def test_current_limit( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
3200    def test_current_limit(self, serial_watcher: SerialWatcher):
3201        """
3202        | Description          | Confirm A fault is raised after 3.5A                                                 |
3203        | :------------------- | :----------------------------------------------------------------------------------- |
3204        | GitHub Issue         | turnaroundfactor/HITL#476                                                            |
3205        | Instructions         | 1. Rest for 30 second                                                           </br>\
3206                                 2. Charge at 3.75 amps (the max limit will be 3.5 amps)                         </br>\
3207                                 3. Verify a prefault and fault are raised.                                      </br>\
3208                                 4. Rest until faults clear                                                      </br>\
3209                                 5. Charge at 3.75 amps                                                          </br>\
3210                                 6. Verify a prefault occurred but not a fault                                        |
3211        | Pass / Fail Criteria | Pass if faults are raised                                                            |
3212        | Estimated Duration   | 2 minutes                                                                            |
3213        | Notes                | We use 3.5A as a limit due to hardware limitations                                   |
3214        """
3215
3216        logger.write_info_to_report("Testing Current Limit")
3217        assert _bms.load and _bms.charger  # Confirm hardware is available
3218
3219        # Rest and make sure no faults are active
3220        logger.write_result_to_html_report("Confirm no faults are active")
3221        serial_watcher.assert_true("flags.prefault_overcurrent_charge", False, 1)
3222        serial_watcher.assert_true("flags.fault_overcurrent_charge", False, 1)
3223
3224        # Charge at 3.25 amps and verify faults are raised
3225        logger.write_result_to_html_report("Charging 3.75 A")
3226        with _bms.charger(16.8, 3.75):
3227            serial_watcher.assert_true("flags.prefault_overcurrent_charge", True, 2, wait_time=60)
3228            serial_watcher.assert_true("flags.fault_overcurrent_charge", True, 2, wait_time=60)
3229
3230        # Rest until faults clear
3231        logger.write_result_to_html_report("Resting")
3232        serial_watcher.assert_true("flags.prefault_overcurrent_charge", False, 3)
3233        serial_watcher.assert_true("flags.fault_overcurrent_charge", False, 3)
3234        assert (
3235            serial_watcher.events["flags.fault_overcurrent_charge"][2].bms_time
3236            - serial_watcher.events["flags.fault_overcurrent_charge"][1].bms_time
3237        ).total_seconds() >= 60
Description Confirm A fault is raised after 3.5A
GitHub Issue turnaroundfactor/HITL#476
Instructions 1. Rest for 30 second
2. Charge at 3.75 amps (the max limit will be 3.5 amps)
3. Verify a prefault and fault are raised.
4. Rest until faults clear
5. Charge at 3.75 amps
6. Verify a prefault occurred but not a fault
Pass / Fail Criteria Pass if faults are raised
Estimated Duration 2 minutes
Notes We use 3.5A as a limit due to hardware limitations
def test_undertemp_charge_rate( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
3239    def test_undertemp_charge_rate(self, serial_watcher: SerialWatcher):
3240        """
3241        | Description          | Confirm a fault is raised at or above 3.1A when below 5°C                            |
3242        | :------------------- | :----------------------------------------------------------------------------------- |
3243        | GitHub Issue         | turnaroundfactor/HITL#611                                                            |
3244        | Instructions         | 1. Rest for 30 second                                                           </br>\
3245                                 2. Charge at 3.3 amps below 5°C                                                 </br>\
3246                                 3. Verify a prefault (before 1 second) and fault (after 1 second) are raised.   </br>\
3247                                 4. Charge above 7°C until faults clear                                               |
3248        | Pass / Fail Criteria | Pass if faults are raised                                                            |
3249        | Estimated Duration   | 2 minutes                                                                            |
3250        """
3251
3252        logger.write_info_to_report("Testing Undertemp charge rate")
3253        assert _bms.load and _bms.charger  # Confirm hardware is available
3254
3255        # Rest and make sure no faults are active
3256        logger.write_result_to_html_report("Confirm no faults are active")
3257        serial_watcher.assert_true("flags.prefault_undertemp_charge_rate", False, 1)
3258        serial_watcher.assert_true("flags.fault_undertemp_charge_rate", False, 1)
3259
3260        # Discharge at 3.1+ amps and verify faults are raised
3261        logger.write_result_to_html_report("Discharging at 3.1A+")
3262        with _bms.charger(16.8, 3.3):
3263            _plateset.thermistor1 = 3
3264            serial_watcher.assert_true("flags.prefault_undertemp_charge_rate", True, 2, wait_time=60)
3265            serial_watcher.assert_true("flags.fault_undertemp_charge_rate", True, 2, wait_time=60)
3266            _plateset.thermistor1 = 8
3267            serial_watcher.assert_true("flags.prefault_undertemp_charge_rate", False, 3, wait_time=90)
3268            serial_watcher.assert_true("flags.fault_undertemp_charge_rate", False, 3, wait_time=90)
Description Confirm a fault is raised at or above 3.1A when below 5°C
GitHub Issue turnaroundfactor/HITL#611
Instructions 1. Rest for 30 second
2. Charge at 3.3 amps below 5°C
3. Verify a prefault (before 1 second) and fault (after 1 second) are raised.
4. Charge above 7°C until faults clear
Pass / Fail Criteria Pass if faults are raised
Estimated Duration 2 minutes
def test_overcurrent_discharge_sustained( self, serial_watcher: hitl_tester.modules.bms.event_watcher.SerialWatcher):
3270    def test_overcurrent_discharge_sustained(self, serial_watcher: SerialWatcher):
3271        """
3272        | Description          | Test Over Current Discharge Sustained Fault                            |
3273        | :------------------- | :--------------------------------------------------------------------- |
3274        | GitHub Issue         | turnaroundfactor/HITL#762                                              |
3275        | Instructions         | 1. Check prefault_sw_overcurrent_discharge and fault_sw_overcurrent_discharge </br>\
3276                                 2. Set current to less than -2.5 amps                             </br>\
3277                                 3. Confirm faults listed in #1 are true                           </br>\
3278                                 4. Set current to 0 amps (rest)                                   </br>\
3279                                 5. Confirm faults listed in #1 are false                               |
3280        | Pass / Fail Criteria | ⦁ Prefault overCurrent Discharge Sustained is False at start      </br>\
3281                                 ⦁ Fault overCurrent Discharge Sustained is False at start         </br>\
3282                                 ⦁ Prefault overCurrent Discharge Sustained is True when current is -2.5 A </br>\
3283                                 ⦁ Fault overCurrent Discharge Sustained is True when current is -2.5 A  </br>\
3284                                 ⦁ Prefault overCurrent Discharge Sustained is False when current is 0 A </br>\
3285                                 ⦁ Fault overCurrent Discharge Sustained is False when current is 0 A   |
3286        | Estimated Duration   | 10 seconds                                                             |
3287        """
3288
3289        logger.write_info_to_report("Testing Overcurrent Discharge Sustained Faults")
3290        assert _bms.load and _bms.charger  # Confirm hardware is available
3291
3292        logger.write_info_to_report("Sw overcurrent protection high current short time")
3293        serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_2", False, 1)
3294        serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_2", False, 1)
3295
3296        logger.write_result_to_html_report("Setting current to less than -2.6 amps")
3297        with _bms.load(2.6):
3298            serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_2", True, 2)
3299            serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_2", True, 2)
3300
3301        logger.write_result_to_html_report("Resting")
3302        serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_2", False, 3, wait_time=60 * 25)
3303        serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_2", False, 3, wait_time=60 * 25)
3304
3305        logger.write_info_to_report("Sw overcurrent protection low current longer time")
3306        serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_1", False, 1)
3307        serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_1", False, 1)
3308
3309        logger.write_result_to_html_report("Setting current to less than -2.4 amps")
3310        with _bms.load(2.4):
3311            serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_1", True, 2)
3312            serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_1", True, 2)
3313
3314        logger.write_result_to_html_report("Resting")
3315        serial_watcher.assert_true("flags.prefault_sw_overcurrent_discharge_1", False, 3, wait_time=60 * 10)
3316        serial_watcher.assert_true("flags.fault_sw_overcurrent_discharge_1", False, 3, wait_time=60 * 10)
Description Test Over Current Discharge Sustained Fault
GitHub Issue turnaroundfactor/HITL#762
Instructions 1. Check prefault_sw_overcurrent_discharge and fault_sw_overcurrent_discharge
2. Set current to less than -2.5 amps
3. Confirm faults listed in #1 are true
4. Set current to 0 amps (rest)
5. Confirm faults listed in #1 are false
Pass / Fail Criteria ⦁ Prefault overCurrent Discharge Sustained is False at start
⦁ Fault overCurrent Discharge Sustained is False at start
⦁ Prefault overCurrent Discharge Sustained is True when current is -2.5 A
⦁ Fault overCurrent Discharge Sustained is True when current is -2.5 A
⦁ Prefault overCurrent Discharge Sustained is False when current is 0 A
⦁ Fault overCurrent Discharge Sustained is False when current is 0 A
Estimated Duration 10 seconds
pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 3.5}]), kwargs={'indirect': True})]
@pytest.mark.parametrize('reset_test_environment', [{'volts': 2.5}], indirect=True)
class TestStateOfCharge:
3319@pytest.mark.parametrize("reset_test_environment", [{"volts": 2.5}], indirect=True)
3320class TestStateOfCharge:
3321    """Confirm cell sim SOC and BMS SOC are within 5%"""
3322
3323    def test_state_of_charge(self):
3324        """
3325        | Description          | Confirm cell sim state of charge and BMS state of charge are within 5% |
3326        | :------------------- | :--------------------------------------------------------------------- |
3327        | GitHub Issue         | turnaroundfactor/HITL#474                                              |
3328        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3329jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D19) |
3330        | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
3331        | Instructions         | 1. Set cell sims SOC to 5%                                        </br>\
3332                                 2. Confirm serial SOC is within 5%                                </br>\
3333                                 3. Increment the cell sim SOC by 1% and repeat steps 1 & 2.            |
3334        | Pass / Fail Criteria | ⦁ Serial state of charge is within 5%                                  |
3335        | Estimated Duration   | 20 seconds                                                             |
3336        | Note                 | When tested as specified in MIL-PERF section 4.7.2.15.1, SMBus data    \
3337                                 output shall be accurate within +0/-5% of the actual state of charge   \
3338                                 for the battery under test throughout the discharge. Manufacturer and  \
3339                                 battery data shall be correctly programmed (see 4.7.2.15.1).           |
3340        """
3341
3342        percent_failed = []
3343        allowed_error = 0.05
3344        max_error = 0.0
3345        failure_rate = []
3346
3347        for percent in range(5, 101):
3348            logger.write_info_to_report(f"Setting cell sims SOC to {percent}%")
3349
3350            for cell in _bms.cells.values():
3351                cell.state_of_charge = percent / 100
3352
3353            time.sleep(2)
3354            serial_monitor.read()  # Clear the latest serial buffer
3355
3356            serial_data = serial_monitor.read()
3357            percent_charged = serial_data["percent_charged"]
3358
3359            max_error = max(max_error, abs(percent_charged - percent))
3360            if not (percent - 5) <= percent_charged <= (percent + 5):
3361                failure_rate.append(1)
3362                logger.write_warning_to_report(
3363                    f"State of Charge is not within {allowed_error:.0%} of {percent}, received {percent_charged}%"
3364                )
3365                percent_failed.append(str(percent))
3366            else:
3367                failure_rate.append(0)
3368                logger.write_info_to_report(
3369                    f"State of Charge is within {allowed_error:.0%} of {percent}% after changing cell sims SOC"
3370                )
3371
3372        if len(percent_failed) > 0:
3373            message = (
3374                f"SOC Error: {max_error / 100:.1%}{allowed_error:.0%} "
3375                f"[{sum(failure_rate) / len(failure_rate):.1%} failed]"
3376            )
3377            logger.write_result_to_html_report(message)
3378            pytest.fail(message)
3379        else:
3380            logger.write_result_to_html_report(f"SOC Error: {max_error:.1%}{allowed_error:.0%}")

Confirm cell sim SOC and BMS SOC are within 5%

def test_state_of_charge(self):
3323    def test_state_of_charge(self):
3324        """
3325        | Description          | Confirm cell sim state of charge and BMS state of charge are within 5% |
3326        | :------------------- | :--------------------------------------------------------------------- |
3327        | GitHub Issue         | turnaroundfactor/HITL#474                                              |
3328        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3329jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D19) |
3330        | MIL-PRF Sections     | 3.5.9.1 (SMBus)                                                        |
3331        | Instructions         | 1. Set cell sims SOC to 5%                                        </br>\
3332                                 2. Confirm serial SOC is within 5%                                </br>\
3333                                 3. Increment the cell sim SOC by 1% and repeat steps 1 & 2.            |
3334        | Pass / Fail Criteria | ⦁ Serial state of charge is within 5%                                  |
3335        | Estimated Duration   | 20 seconds                                                             |
3336        | Note                 | When tested as specified in MIL-PERF section 4.7.2.15.1, SMBus data    \
3337                                 output shall be accurate within +0/-5% of the actual state of charge   \
3338                                 for the battery under test throughout the discharge. Manufacturer and  \
3339                                 battery data shall be correctly programmed (see 4.7.2.15.1).           |
3340        """
3341
3342        percent_failed = []
3343        allowed_error = 0.05
3344        max_error = 0.0
3345        failure_rate = []
3346
3347        for percent in range(5, 101):
3348            logger.write_info_to_report(f"Setting cell sims SOC to {percent}%")
3349
3350            for cell in _bms.cells.values():
3351                cell.state_of_charge = percent / 100
3352
3353            time.sleep(2)
3354            serial_monitor.read()  # Clear the latest serial buffer
3355
3356            serial_data = serial_monitor.read()
3357            percent_charged = serial_data["percent_charged"]
3358
3359            max_error = max(max_error, abs(percent_charged - percent))
3360            if not (percent - 5) <= percent_charged <= (percent + 5):
3361                failure_rate.append(1)
3362                logger.write_warning_to_report(
3363                    f"State of Charge is not within {allowed_error:.0%} of {percent}, received {percent_charged}%"
3364                )
3365                percent_failed.append(str(percent))
3366            else:
3367                failure_rate.append(0)
3368                logger.write_info_to_report(
3369                    f"State of Charge is within {allowed_error:.0%} of {percent}% after changing cell sims SOC"
3370                )
3371
3372        if len(percent_failed) > 0:
3373            message = (
3374                f"SOC Error: {max_error / 100:.1%}{allowed_error:.0%} "
3375                f"[{sum(failure_rate) / len(failure_rate):.1%} failed]"
3376            )
3377            logger.write_result_to_html_report(message)
3378            pytest.fail(message)
3379        else:
3380            logger.write_result_to_html_report(f"SOC Error: {max_error:.1%}{allowed_error:.0%}")
Description Confirm cell sim state of charge and BMS state of charge are within 5%
GitHub Issue turnaroundfactor/HITL#474
Google Docs Google Sheet Cell
MIL-PRF Sections 3.5.9.1 (SMBus)
Instructions 1. Set cell sims SOC to 5%
2. Confirm serial SOC is within 5%
3. Increment the cell sim SOC by 1% and repeat steps 1 & 2.
Pass / Fail Criteria ⦁ Serial state of charge is within 5%
Estimated Duration 20 seconds
Note When tested as specified in MIL-PERF section 4.7.2.15.1, SMBus data output shall be accurate within +0/-5% of the actual state of charge for the battery under test throughout the discharge. Manufacturer and battery data shall be correctly programmed (see 4.7.2.15.1).
pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 2.5}]), kwargs={'indirect': True})]
@pytest.mark.parametrize('reset_test_environment', [{'volts': 3.7, 'temperature': 23}], indirect=True)
class TestTemperatureAccuracy:
3383@pytest.mark.parametrize("reset_test_environment", [{"volts": 3.7, "temperature": 23}], indirect=True)
3384class TestTemperatureAccuracy:
3385    """Compare BMS and HITL temps."""
3386
3387    class TemperatureDiscrepancyTherm1(CSVRecordEvent):
3388        """@private Compare HITL temperature to reported temperature."""
3389
3390        allowable_error = 5.0
3391        max = SimpleNamespace(hitl_c=0, bms_c=0, error=0.0)
3392
3393        @classmethod
3394        def failed(cls) -> bool:
3395            """Check if test parameters were exceeded."""
3396            return bool(cls.max.error > cls.allowable_error)
3397
3398        @classmethod
3399        def verify(cls, _row, serial_data, _cell_data):
3400            """Temperature within range"""
3401            row_data = SimpleNamespace(hitl_c=_plateset.thermistor1, bms_c=serial_data["dk_temp"] / 10 - 273)
3402            row_data.error = abs(row_data.bms_c - row_data.hitl_c)
3403            cls.max = max(cls.max, row_data, key=lambda data: data.error)
3404
3405        @classmethod
3406        def result(cls):
3407            """Detailed test result information."""
3408            return (
3409                f"Thermistor 1 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error, '°C', '.2f')}"
3410                f"(HITL: {cls.max.hitl_c:.2f} °C, BMS: {cls.max.bms_c:.2f} °C)"
3411            )
3412
3413    class TemperatureDiscrepancyTherm2(CSVRecordEvent):
3414        """@private Compare HITL temperature to reported temperature."""
3415
3416        allowable_error = 5.0
3417        max = SimpleNamespace(hitl_c=0, bms_c=0, error=0.0)
3418
3419        @classmethod
3420        def failed(cls) -> bool:
3421            """Check if test parameters were exceeded."""
3422            return bool(cls.max.error > cls.allowable_error)
3423
3424        @classmethod
3425        def verify(cls, _row, serial_data, _cell_data):
3426            """Temperature within range"""
3427            row_data = SimpleNamespace(hitl_c=_plateset.thermistor2, bms_c=serial_data["dk_temp1"] / 10 - 273)
3428            row_data.error = abs(row_data.bms_c - row_data.hitl_c)
3429            cls.max = max(cls.max, row_data, key=lambda data: data.error)
3430
3431        @classmethod
3432        def result(cls):
3433            """Detailed test result information."""
3434            return (
3435                f"Thermistor 2 error: {cls.cmp(cls.max.error, '<=', cls.allowable_error, '°C', '.2f')}"
3436                f"(HITL: {cls.max.hitl_c:.2f} °C, BMS: {cls.max.bms_c:.2f} °C)"
3437            )
3438
3439    def test_temperature_accuracy(self):
3440        """
3441         | Description          | TAF: Ensure that temperature measurements are accurate                       |
3442         | :------------------- | :--------------------------------------------------------------------------- |
3443         | GitHub Issue         | turnaroundfactor/HITL#401                                                    |
3444         | MIL-PRF Section      | 3.5.8.3 (Accuracy)                                                      </br>\
3445                                  4.7.2.14.3 (Accuracy During Discharge)                                       |
3446         | Instructions         | 1. Set THERM1 and THERM2 to -40C                                        </br>\
3447                                  2. Set cell voltages to 3.7V per cell                                   </br>\
3448                                  3. Increment THERM1 and THERM2 in 5C increments up to and including 60C </br>\
3449                                  4.  Record the following data at each current increment                 </br>\
3450                                      HITL: THERM1 Measurement, THERM2 Measurement                        </br>\
3451                                      SERIAL: THERM1, THERM2"                                                  |
3452         | Pass / Fail Criteria | HITL and Serial are within 5%                                                |
3453         | Estimated Duration   | 1 minute                                                                     |
3454         """
3455        _bms.timer.reset()  # Keep track of runtime
3456        _plateset.disengage_safety_protocols = True
3457
3458        for target_c in range(-40, 65, 5):
3459            _plateset.thermistor1 = _plateset.thermistor2 = target_c
3460            time.sleep(1)
3461            logger.write_info_to_report(
3462                f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, "
3463                f"Temp1: {_plateset.thermistor1}, Temp2: {_plateset.thermistor2}"
3464            )
3465            _bms.csv.cycle.record(_bms.timer.elapsed_time)
3466            time.sleep(0.5)
3467
3468        # Check results
3469        if CSVRecordEvent.failed():
3470            pytest.fail(CSVRecordEvent.result())

Compare BMS and HITL temps.

def test_temperature_accuracy(self):
3439    def test_temperature_accuracy(self):
3440        """
3441         | Description          | TAF: Ensure that temperature measurements are accurate                       |
3442         | :------------------- | :--------------------------------------------------------------------------- |
3443         | GitHub Issue         | turnaroundfactor/HITL#401                                                    |
3444         | MIL-PRF Section      | 3.5.8.3 (Accuracy)                                                      </br>\
3445                                  4.7.2.14.3 (Accuracy During Discharge)                                       |
3446         | Instructions         | 1. Set THERM1 and THERM2 to -40C                                        </br>\
3447                                  2. Set cell voltages to 3.7V per cell                                   </br>\
3448                                  3. Increment THERM1 and THERM2 in 5C increments up to and including 60C </br>\
3449                                  4.  Record the following data at each current increment                 </br>\
3450                                      HITL: THERM1 Measurement, THERM2 Measurement                        </br>\
3451                                      SERIAL: THERM1, THERM2"                                                  |
3452         | Pass / Fail Criteria | HITL and Serial are within 5%                                                |
3453         | Estimated Duration   | 1 minute                                                                     |
3454         """
3455        _bms.timer.reset()  # Keep track of runtime
3456        _plateset.disengage_safety_protocols = True
3457
3458        for target_c in range(-40, 65, 5):
3459            _plateset.thermistor1 = _plateset.thermistor2 = target_c
3460            time.sleep(1)
3461            logger.write_info_to_report(
3462                f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, "
3463                f"Temp1: {_plateset.thermistor1}, Temp2: {_plateset.thermistor2}"
3464            )
3465            _bms.csv.cycle.record(_bms.timer.elapsed_time)
3466            time.sleep(0.5)
3467
3468        # Check results
3469        if CSVRecordEvent.failed():
3470            pytest.fail(CSVRecordEvent.result())
Description TAF: Ensure that temperature measurements are accurate
GitHub Issue turnaroundfactor/HITL#401
MIL-PRF Section 3.5.8.3 (Accuracy)
4.7.2.14.3 (Accuracy During Discharge)
Instructions 1. Set THERM1 and THERM2 to -40C
2. Set cell voltages to 3.7V per cell
3. Increment THERM1 and THERM2 in 5C increments up to and including 60C
4. Record the following data at each current increment
HITL: THERM1 Measurement, THERM2 Measurement
SERIAL: THERM1, THERM2"
Pass / Fail Criteria HITL and Serial are within 5%
Estimated Duration 1 minute
pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 3.7, 'temperature': 23}]), kwargs={'indirect': True})]
@pytest.mark.parametrize('reset_test_environment', [{'volts': 3.7, 'temperature': 23}], indirect=True)
class TestCurrentAccuracy:
3473@pytest.mark.parametrize("reset_test_environment", [{"volts": 3.7, "temperature": 23}], indirect=True)
3474class TestCurrentAccuracy:
3475    """Run a test for current accuracy"""
3476
3477    class CurrentCellAccuracy(CSVRecordEvent):
3478        """@private Compare terminal current to reported current."""
3479
3480        allowable_error = 0.01
3481        max = SimpleNamespace(hitl_a=0, bms_a=0, error=0.0)
3482
3483        @classmethod
3484        def failed(cls) -> bool:
3485            """Check if test parameters were exceeded."""
3486            return bool(cls.max.error > cls.allowable_error)
3487
3488        @classmethod
3489        def verify(cls, row, serial_data, _cell_data):
3490            """Current within range"""
3491            if not _plateset.load_switch:
3492                row_data = SimpleNamespace(hitl_a=row["HITL Current (A)"], bms_a=serial_data["mamps"] / 1000)
3493            else:
3494                row_data = SimpleNamespace(hitl_a=-row["HITL Current (A)"], bms_a=serial_data["mamps"] / 1000)
3495            row_data.error = abs((row_data.bms_a - row_data.hitl_a) / row_data.hitl_a)
3496            if abs(row_data.hitl_a) > 0.100:  # Ignore currents within 100mA to -100mA
3497                cls.max = max(cls.max, row_data, key=lambda data: data.error)
3498
3499        @classmethod
3500        def result(cls):
3501            """Detailed test result information."""
3502            return (
3503                f"Current error: {cls.cmp(cls.max.error, '<=', cls.allowable_error)} "
3504                f"(HITL: {cls.max.hitl_a * 1000:.3f} mA, BMS: {cls.max.bms_a * 1000:.3f} mA)"
3505            )
3506
3507    def test_current_accuracy(self):
3508        """
3509        | Description          | Test the cell current accuracy                                         |
3510        | :------------------- | :--------------------------------------------------------------------- |
3511        | GitHub Issue         | turnaroundfactor/HITL#400                                              |
3512        | MIL-PRF Sections     | 3.5.8.3 (Accuracy)                                                </br>\
3513                                 4.7.2.14.3 (Accuracy During Discharge)                                 |
3514        | Instructions         | 1. Set thermistors to 23C                                              |
3515                                 2. Set cell voltages to 3.7V per cell                                  |
3516                                 3. Increment the charing current from 100mA to 3A in 50mA increments   |
3517                                 4. Increment the discharging current from 100mA to 3A in 50 mA         |
3518                                        increments                                                      |
3519                                 5. Record the following data at each current increment                 |
3520                                     HITL: Current (A)                                                  |
3521                                     SERIAL: Current (A)                                                |
3522        | Pass / Fail Criteria | Pass IF:                                                               |
3523                                    SERIAL Current measurements agree with the HITL Terminal Current    |
3524                                    measurements to within 1% for abs(Terminal Current >= 100mA)        |
3525                                    - Result highest current (mA) discrepancy                           |
3526        | Estimated Duration   | ??                                                                     |
3527        | Note                 | ??                                                                     |
3528        """
3529        _bms.timer.reset()  # Keep track of runtime
3530
3531        with _bms.charger(16.8, 0.1):
3532            for target_ma in range(100, 2050, 50):
3533                _bms.charger.amps = target_ma / 1000
3534                time.sleep(1)
3535                logger.write_info_to_report(
3536                    f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Current: {_bms.charger.amps:.3f}"
3537                )
3538                time.sleep(1)
3539                _bms.csv.cycle.record(_bms.timer.elapsed_time, _bms.charger.amps)
3540                time.sleep(1)
3541
3542        with _bms.load(0.1):
3543            for target_ma in range(100, 2050, 50):
3544                time.sleep(1)
3545                _bms.load.amps = target_ma / 1000
3546                time.sleep(1)
3547                logger.write_info_to_report(
3548                    f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Current: {_bms.load.amps:.3f}"
3549                )
3550                time.sleep(1)
3551                _bms.csv.cycle.record(_bms.timer.elapsed_time, _bms.load.amps)
3552
3553        if CSVRecordEvent.failed():
3554            pytest.fail(CSVRecordEvent.result())

Run a test for current accuracy

def test_current_accuracy(self):
3507    def test_current_accuracy(self):
3508        """
3509        | Description          | Test the cell current accuracy                                         |
3510        | :------------------- | :--------------------------------------------------------------------- |
3511        | GitHub Issue         | turnaroundfactor/HITL#400                                              |
3512        | MIL-PRF Sections     | 3.5.8.3 (Accuracy)                                                </br>\
3513                                 4.7.2.14.3 (Accuracy During Discharge)                                 |
3514        | Instructions         | 1. Set thermistors to 23C                                              |
3515                                 2. Set cell voltages to 3.7V per cell                                  |
3516                                 3. Increment the charing current from 100mA to 3A in 50mA increments   |
3517                                 4. Increment the discharging current from 100mA to 3A in 50 mA         |
3518                                        increments                                                      |
3519                                 5. Record the following data at each current increment                 |
3520                                     HITL: Current (A)                                                  |
3521                                     SERIAL: Current (A)                                                |
3522        | Pass / Fail Criteria | Pass IF:                                                               |
3523                                    SERIAL Current measurements agree with the HITL Terminal Current    |
3524                                    measurements to within 1% for abs(Terminal Current >= 100mA)        |
3525                                    - Result highest current (mA) discrepancy                           |
3526        | Estimated Duration   | ??                                                                     |
3527        | Note                 | ??                                                                     |
3528        """
3529        _bms.timer.reset()  # Keep track of runtime
3530
3531        with _bms.charger(16.8, 0.1):
3532            for target_ma in range(100, 2050, 50):
3533                _bms.charger.amps = target_ma / 1000
3534                time.sleep(1)
3535                logger.write_info_to_report(
3536                    f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Current: {_bms.charger.amps:.3f}"
3537                )
3538                time.sleep(1)
3539                _bms.csv.cycle.record(_bms.timer.elapsed_time, _bms.charger.amps)
3540                time.sleep(1)
3541
3542        with _bms.load(0.1):
3543            for target_ma in range(100, 2050, 50):
3544                time.sleep(1)
3545                _bms.load.amps = target_ma / 1000
3546                time.sleep(1)
3547                logger.write_info_to_report(
3548                    f"Elapsed Time: {_bms.timer.elapsed_time:.3f}, Current: {_bms.load.amps:.3f}"
3549                )
3550                time.sleep(1)
3551                _bms.csv.cycle.record(_bms.timer.elapsed_time, _bms.load.amps)
3552
3553        if CSVRecordEvent.failed():
3554            pytest.fail(CSVRecordEvent.result())
Description Test the cell current accuracy
GitHub Issue turnaroundfactor/HITL#400
MIL-PRF Sections 3.5.8.3 (Accuracy)
4.7.2.14.3 (Accuracy During Discharge)
Instructions 1. Set thermistors to 23C

2. Set cell voltages to 3.7V per cell | 3. Increment the charing current from 100mA to 3A in 50mA increments | 4. Increment the discharging current from 100mA to 3A in 50 mA | increments | 5. Record the following data at each current increment | HITL: Current (A) | SERIAL: Current (A) | | Pass / Fail Criteria | Pass IF: | SERIAL Current measurements agree with the HITL Terminal Current | measurements to within 1% for abs(Terminal Current >= 100mA) | - Result highest current (mA) discrepancy | | Estimated Duration | ?? | | Note | ?? |

pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 3.7, 'temperature': 23}]), kwargs={'indirect': True})]
def battery0_voltage_check(serial_data: dict[str, int | bool | str]):
3557def battery0_voltage_check(serial_data: dict[str, int | bool | str]):
3558    """Checks the SERIAL Battery 0 Voltage"""
3559    expected_charge = 14.8
3560    voltage_low_range = expected_charge - 0.100
3561    voltage_high_range = expected_charge + 2
3562    voltage_value = f"{expected_charge}V  -100mV/+2V"
3563
3564    serial_voltage = float(serial_data["mvolt_battery"]) / 1000
3565
3566    if voltage_low_range <= serial_voltage <= voltage_high_range:
3567        logger.write_result_to_html_report(
3568            f"Battery 0 Voltage is {serial_voltage}V at the start of this test, "
3569            f"which is within within range of: {voltage_value}"
3570        )
3571
3572    else:
3573        logger.write_failure_to_html_report(
3574            f"Battery 0 Voltage is {serial_voltage}V at the start of this test, "
3575            f"which is not within within range of: {voltage_value}"
3576        )
3577        pytest.fail()

Checks the SERIAL Battery 0 Voltage

@pytest.mark.parametrize('reset_test_environment', [{'volts': 3.7, 'temperature': 23}], indirect=True)
class TestCeaseCharging(hitl_tester.modules.bms.test_handler.CSVRecordEvent):
3580@pytest.mark.parametrize("reset_test_environment", [{"volts": 3.7, "temperature": 23}], indirect=True)
3581class TestCeaseCharging(CSVRecordEvent):
3582    """Run a test to cease charging after"""
3583
3584    def test_cease_charging(self):
3585        """
3586        | Description          | Cease charging if charger keeps trying too long                        |
3587        | :------------------- | :--------------------------------------------------------------------- |
3588        | GitHub Issue         | turnaroundfactor/HITL#745                                       |
3589        #TODO: Update MIL-PRF sections
3590        | MIL-PRF Sections     | 3.5.8.3 (Accuracy)                                                </br>\
3591                                 4.7.2.14.3 (Accuracy During Discharge)                                 |
3592        | Instructions         | 1. Set thermistors to 23C                                         </br>\
3593                                 2. Put cells in a rested state at 3.7V per cell                   </br>\
3594                                 3. Charge at 40 mA (do not let charger side terminate charge)     </br>\
3595                                 4. Wait 3,480 seconds (0:58 HR:MIN)                              </br>\
3596                                 5. Wait 240 seconds (1:02 HR:MIN)                                 </br>\
3597                                 6. Disable CE                                                     </br>\
3598                                 7. Wait 5 Seconds                                                 </br>\
3599                                 8. Enable CE                                                      </br>\
3600                                 9. Wait 5 seconds                                                 </br>\
3601                                 10. Attempt to charge at 2A                                            |
3602        | Pass / Fail Criteria | Pass IF at Step #...                                              </br>\
3603                                 1. SERIAL THERM1 and THERM2 are 23C +/- 1.1C                      </br>\
3604                                 2. SERIAL Battery 0 Voltage is 14.8V +/- 100 mV                   </br>\
3605                                 3. HITL Charge Current is 20 mA +70mA / -0mA                     </br>\
3606                                 4. SERIAL Battery 0 Voltage is 14.8V +/- 100 mV                   </br>\
3607                                 4. HITL Charge Current is 20 mA +70mA / -0mA                      </br>\
3608                                 4. SERIAL No Fault Flags                                          </br>\
3609                                 5. SERIAL Battery 0 Voltage is 14.8V +/- 100 mV                   </br>\
3610                                 5. HITL Charge Current is 0 mA +/- 1mA                            </br>\
3611                                 5. SERIAL Flag OverTime_Charge is flagged                         </br>\
3612                                 10. HITL Charge Current is 2A +/- 30mA                                 |
3613        | Estimated Duration   | 64 minutes                                                            |
3614        | Note                 | ??                                                                     |
3615        """
3616        serial_data = serial_monitor.read()
3617
3618        set_temp = 23
3619        # Check THERM 1 & Therm 2
3620        therm_one = serial_data["dk_temp"] / 10 - 273
3621        therm_two = serial_data["dk_temp1"] / 10 - 273
3622        low_range = set_temp - 1.1
3623        high_range = set_temp + 1.1
3624        temp_range = f"{set_temp}°C +/- 1.1°C"
3625
3626        if low_range <= therm_one <= high_range:
3627            logger.write_result_to_html_report(
3628                f"THERM1 was {therm_one:.1f}°C, which was within the expected range of {temp_range}"
3629            )
3630        else:
3631            logger.write_failure_to_html_report(
3632                f"THERM1 was {therm_one:.1f}°C, which was not within the expected range of {temp_range}"
3633            )
3634            pytest.fail()
3635
3636        if low_range <= therm_two <= high_range:
3637            logger.write_result_to_html_report(
3638                f"THERM2 was {therm_two:.1f}°C, which was within the expected range of {temp_range}"
3639            )
3640        else:
3641            logger.write_failure_to_html_report(
3642                f"THERM2 was {therm_two:.1f}°C, which was not within the expected range of {temp_range}"
3643            )
3644            pytest.fail()
3645
3646        # Check Serial Battery 0 Voltage:
3647        battery0_voltage_check(serial_data)
3648
3649        # Charge at 40 mA -- keep voltage at 3.7
3650        with _bms.charger(16.8, 0.040):
3651            logger.write_info_to_report("Charging at 40mA")
3652            time.sleep(60)
3653
3654            high_current_range = 0.020 + 0.070
3655            low_current_range = 0.020
3656
3657            expected_current_range = "20mA +70mA/-0mA"
3658            terminal_current = _bms.charger.amps
3659
3660            if low_current_range <= terminal_current <= high_current_range:
3661                logger.write_result_to_html_report(
3662                    f"HITL Terminal Current was {terminal_current:.3f}A after charging, which was within the expected "
3663                    f"range of {expected_current_range}"
3664                )
3665            else:
3666                logger.write_failure_to_html_report(
3667                    f"HITL Terminal Current was {terminal_current:.3f}A after charging, which was not within the "
3668                    f"expected range of {expected_current_range}"
3669                )
3670                pytest.fail()
3671
3672            # 1 hour (debug mode)
3673            long_rest = 3480
3674
3675            logger.write_info_to_report(f"Waiting for {long_rest} seconds")
3676            time.sleep(long_rest)
3677
3678            serial_data = serial_monitor.read()
3679
3680            # Check Battery 0 Voltage
3681            battery0_voltage_check(serial_data)
3682
3683            # Check HITL Charge Current
3684            terminal_current = _bms.charger.amps
3685
3686            if low_current_range <= terminal_current <= high_current_range:
3687                logger.write_result_to_html_report(
3688                    f"HITL Terminal Current was {terminal_current:.3f}A after waiting {long_rest} seconds, "
3689                    f"which was within the expected range of {expected_current_range}"
3690                )
3691            else:
3692                logger.write_failure_to_html_report(
3693                    f"HITL Terminal Current was {terminal_current:.3f}A after waiting {long_rest} seconds, "
3694                    f"which was not within the expected range of {expected_current_range}"
3695                )
3696                pytest.fail()
3697
3698            # Check Serial OverTime_Charge
3699            no_fault_flag = serial_data["flags.fault_overtime_charge"]
3700            if no_fault_flag is False:
3701                logger.write_result_to_html_report(
3702                    f"OverTime Charge Fault was False, the expected value after waiting {long_rest} seconds"
3703                )
3704            else:
3705                logger.write_failure_to_html_report(
3706                    f"OverTime Charge Fault was True, which was not expected after waiting {long_rest} seconds"
3707                )
3708                pytest.fail()
3709
3710            # Sleep for 240 seconds
3711            short_rest = 240
3712            logger.write_info_to_report(f"Waiting for {short_rest} seconds")
3713            time.sleep(short_rest)
3714
3715            serial_data = serial_monitor.read()
3716
3717            # Check Battery 0 Voltage
3718            battery0_voltage_check(serial_data)
3719
3720            # Check HITL Charge Current
3721            expected_charge = 0
3722            high_current_range = expected_charge + 0.020
3723            low_current_range = expected_charge - 0.020
3724
3725            expected_current_range = "0mA +/- 20mA"
3726            terminal_current = _bms.charger.amps
3727
3728            if low_current_range <= terminal_current <= high_current_range:
3729                logger.write_result_to_html_report(
3730                    f"HITL Terminal Current was {terminal_current:.3f}A after waiting {short_rest} seconds, "
3731                    f"which was within the expected range of {expected_current_range}"
3732                )
3733            else:
3734                logger.write_failure_to_html_report(
3735                    f"HITL Terminal Current was {terminal_current:.3f}A after waiting {short_rest} seconds, "
3736                    f"which was not within the expected range of {expected_current_range}"
3737                )
3738                pytest.fail()
3739
3740            # Check Overtime Charge Flag
3741            no_fault_flag = serial_data["flags.fault_overtime_charge"]
3742            if no_fault_flag is True:
3743                logger.write_result_to_html_report(
3744                    f"Overtime Charge Fault was True, the expected value after waiting {short_rest} seconds"
3745                )
3746            else:
3747                logger.write_failure_to_html_report(
3748                    f"Overtime Charge Fault was False, which was not expected after waiting {short_rest} seconds"
3749                )
3750                pytest.fail()
3751
3752        # Wait 5 Seconds (Since BMS_Charger is disabled)
3753        time.sleep(5)
3754
3755        # Enable BMS Charger & Wait 5 Seconds before attempting to charge
3756        with _bms.charger(16.8, 2):
3757            time.sleep(5)
3758
3759            # Check HITL Charge Current
3760            terminal_current = _bms.charger.amps
3761            expected_current = 2
3762            expected_current_range = "2A +/- 30mA"
3763            high_current_range = expected_current + 0.03
3764            low_current_range = expected_current
3765
3766            if low_current_range <= terminal_current <= high_current_range:
3767                logger.write_result_to_html_report(
3768                    f"HITL Terminal Current was {terminal_current:.3f}A after disabling/enabling charge, "
3769                    f"which was within the expected range of {expected_current_range}"
3770                )
3771            else:
3772                logger.write_failure_to_html_report(
3773                    f"HITL Terminal Current was {terminal_current:.3f}A after disabling/enabling charge, "
3774                    f"which was not within the expected range of {expected_current_range}"
3775                )
3776                pytest.fail()

Run a test to cease charging after

def test_cease_charging(self):
3584    def test_cease_charging(self):
3585        """
3586        | Description          | Cease charging if charger keeps trying too long                        |
3587        | :------------------- | :--------------------------------------------------------------------- |
3588        | GitHub Issue         | turnaroundfactor/HITL#745                                       |
3589        #TODO: Update MIL-PRF sections
3590        | MIL-PRF Sections     | 3.5.8.3 (Accuracy)                                                </br>\
3591                                 4.7.2.14.3 (Accuracy During Discharge)                                 |
3592        | Instructions         | 1. Set thermistors to 23C                                         </br>\
3593                                 2. Put cells in a rested state at 3.7V per cell                   </br>\
3594                                 3. Charge at 40 mA (do not let charger side terminate charge)     </br>\
3595                                 4. Wait 3,480 seconds (0:58 HR:MIN)                              </br>\
3596                                 5. Wait 240 seconds (1:02 HR:MIN)                                 </br>\
3597                                 6. Disable CE                                                     </br>\
3598                                 7. Wait 5 Seconds                                                 </br>\
3599                                 8. Enable CE                                                      </br>\
3600                                 9. Wait 5 seconds                                                 </br>\
3601                                 10. Attempt to charge at 2A                                            |
3602        | Pass / Fail Criteria | Pass IF at Step #...                                              </br>\
3603                                 1. SERIAL THERM1 and THERM2 are 23C +/- 1.1C                      </br>\
3604                                 2. SERIAL Battery 0 Voltage is 14.8V +/- 100 mV                   </br>\
3605                                 3. HITL Charge Current is 20 mA +70mA / -0mA                     </br>\
3606                                 4. SERIAL Battery 0 Voltage is 14.8V +/- 100 mV                   </br>\
3607                                 4. HITL Charge Current is 20 mA +70mA / -0mA                      </br>\
3608                                 4. SERIAL No Fault Flags                                          </br>\
3609                                 5. SERIAL Battery 0 Voltage is 14.8V +/- 100 mV                   </br>\
3610                                 5. HITL Charge Current is 0 mA +/- 1mA                            </br>\
3611                                 5. SERIAL Flag OverTime_Charge is flagged                         </br>\
3612                                 10. HITL Charge Current is 2A +/- 30mA                                 |
3613        | Estimated Duration   | 64 minutes                                                            |
3614        | Note                 | ??                                                                     |
3615        """
3616        serial_data = serial_monitor.read()
3617
3618        set_temp = 23
3619        # Check THERM 1 & Therm 2
3620        therm_one = serial_data["dk_temp"] / 10 - 273
3621        therm_two = serial_data["dk_temp1"] / 10 - 273
3622        low_range = set_temp - 1.1
3623        high_range = set_temp + 1.1
3624        temp_range = f"{set_temp}°C +/- 1.1°C"
3625
3626        if low_range <= therm_one <= high_range:
3627            logger.write_result_to_html_report(
3628                f"THERM1 was {therm_one:.1f}°C, which was within the expected range of {temp_range}"
3629            )
3630        else:
3631            logger.write_failure_to_html_report(
3632                f"THERM1 was {therm_one:.1f}°C, which was not within the expected range of {temp_range}"
3633            )
3634            pytest.fail()
3635
3636        if low_range <= therm_two <= high_range:
3637            logger.write_result_to_html_report(
3638                f"THERM2 was {therm_two:.1f}°C, which was within the expected range of {temp_range}"
3639            )
3640        else:
3641            logger.write_failure_to_html_report(
3642                f"THERM2 was {therm_two:.1f}°C, which was not within the expected range of {temp_range}"
3643            )
3644            pytest.fail()
3645
3646        # Check Serial Battery 0 Voltage:
3647        battery0_voltage_check(serial_data)
3648
3649        # Charge at 40 mA -- keep voltage at 3.7
3650        with _bms.charger(16.8, 0.040):
3651            logger.write_info_to_report("Charging at 40mA")
3652            time.sleep(60)
3653
3654            high_current_range = 0.020 + 0.070
3655            low_current_range = 0.020
3656
3657            expected_current_range = "20mA +70mA/-0mA"
3658            terminal_current = _bms.charger.amps
3659
3660            if low_current_range <= terminal_current <= high_current_range:
3661                logger.write_result_to_html_report(
3662                    f"HITL Terminal Current was {terminal_current:.3f}A after charging, which was within the expected "
3663                    f"range of {expected_current_range}"
3664                )
3665            else:
3666                logger.write_failure_to_html_report(
3667                    f"HITL Terminal Current was {terminal_current:.3f}A after charging, which was not within the "
3668                    f"expected range of {expected_current_range}"
3669                )
3670                pytest.fail()
3671
3672            # 1 hour (debug mode)
3673            long_rest = 3480
3674
3675            logger.write_info_to_report(f"Waiting for {long_rest} seconds")
3676            time.sleep(long_rest)
3677
3678            serial_data = serial_monitor.read()
3679
3680            # Check Battery 0 Voltage
3681            battery0_voltage_check(serial_data)
3682
3683            # Check HITL Charge Current
3684            terminal_current = _bms.charger.amps
3685
3686            if low_current_range <= terminal_current <= high_current_range:
3687                logger.write_result_to_html_report(
3688                    f"HITL Terminal Current was {terminal_current:.3f}A after waiting {long_rest} seconds, "
3689                    f"which was within the expected range of {expected_current_range}"
3690                )
3691            else:
3692                logger.write_failure_to_html_report(
3693                    f"HITL Terminal Current was {terminal_current:.3f}A after waiting {long_rest} seconds, "
3694                    f"which was not within the expected range of {expected_current_range}"
3695                )
3696                pytest.fail()
3697
3698            # Check Serial OverTime_Charge
3699            no_fault_flag = serial_data["flags.fault_overtime_charge"]
3700            if no_fault_flag is False:
3701                logger.write_result_to_html_report(
3702                    f"OverTime Charge Fault was False, the expected value after waiting {long_rest} seconds"
3703                )
3704            else:
3705                logger.write_failure_to_html_report(
3706                    f"OverTime Charge Fault was True, which was not expected after waiting {long_rest} seconds"
3707                )
3708                pytest.fail()
3709
3710            # Sleep for 240 seconds
3711            short_rest = 240
3712            logger.write_info_to_report(f"Waiting for {short_rest} seconds")
3713            time.sleep(short_rest)
3714
3715            serial_data = serial_monitor.read()
3716
3717            # Check Battery 0 Voltage
3718            battery0_voltage_check(serial_data)
3719
3720            # Check HITL Charge Current
3721            expected_charge = 0
3722            high_current_range = expected_charge + 0.020
3723            low_current_range = expected_charge - 0.020
3724
3725            expected_current_range = "0mA +/- 20mA"
3726            terminal_current = _bms.charger.amps
3727
3728            if low_current_range <= terminal_current <= high_current_range:
3729                logger.write_result_to_html_report(
3730                    f"HITL Terminal Current was {terminal_current:.3f}A after waiting {short_rest} seconds, "
3731                    f"which was within the expected range of {expected_current_range}"
3732                )
3733            else:
3734                logger.write_failure_to_html_report(
3735                    f"HITL Terminal Current was {terminal_current:.3f}A after waiting {short_rest} seconds, "
3736                    f"which was not within the expected range of {expected_current_range}"
3737                )
3738                pytest.fail()
3739
3740            # Check Overtime Charge Flag
3741            no_fault_flag = serial_data["flags.fault_overtime_charge"]
3742            if no_fault_flag is True:
3743                logger.write_result_to_html_report(
3744                    f"Overtime Charge Fault was True, the expected value after waiting {short_rest} seconds"
3745                )
3746            else:
3747                logger.write_failure_to_html_report(
3748                    f"Overtime Charge Fault was False, which was not expected after waiting {short_rest} seconds"
3749                )
3750                pytest.fail()
3751
3752        # Wait 5 Seconds (Since BMS_Charger is disabled)
3753        time.sleep(5)
3754
3755        # Enable BMS Charger & Wait 5 Seconds before attempting to charge
3756        with _bms.charger(16.8, 2):
3757            time.sleep(5)
3758
3759            # Check HITL Charge Current
3760            terminal_current = _bms.charger.amps
3761            expected_current = 2
3762            expected_current_range = "2A +/- 30mA"
3763            high_current_range = expected_current + 0.03
3764            low_current_range = expected_current
3765
3766            if low_current_range <= terminal_current <= high_current_range:
3767                logger.write_result_to_html_report(
3768                    f"HITL Terminal Current was {terminal_current:.3f}A after disabling/enabling charge, "
3769                    f"which was within the expected range of {expected_current_range}"
3770                )
3771            else:
3772                logger.write_failure_to_html_report(
3773                    f"HITL Terminal Current was {terminal_current:.3f}A after disabling/enabling charge, "
3774                    f"which was not within the expected range of {expected_current_range}"
3775                )
3776                pytest.fail()
Description Cease charging if charger keeps trying too long
GitHub Issue turnaroundfactor/HITL#745

TODO: Update MIL-PRF sections

| MIL-PRF Sections | 3.5.8.3 (Accuracy)
4.7.2.14.3 (Accuracy During Discharge) | | Instructions | 1. Set thermistors to 23C
2. Put cells in a rested state at 3.7V per cell
3. Charge at 40 mA (do not let charger side terminate charge)
4. Wait 3,480 seconds (0:58 HR:MIN)
5. Wait 240 seconds (1:02 HR:MIN)
6. Disable CE
7. Wait 5 Seconds
8. Enable CE
9. Wait 5 seconds
10. Attempt to charge at 2A | | Pass / Fail Criteria | Pass IF at Step #...
1. SERIAL THERM1 and THERM2 are 23C +/- 1.1C
2. SERIAL Battery 0 Voltage is 14.8V +/- 100 mV
3. HITL Charge Current is 20 mA +70mA / -0mA
4. SERIAL Battery 0 Voltage is 14.8V +/- 100 mV
4. HITL Charge Current is 20 mA +70mA / -0mA
4. SERIAL No Fault Flags
5. SERIAL Battery 0 Voltage is 14.8V +/- 100 mV
5. HITL Charge Current is 0 mA +/- 1mA
5. SERIAL Flag OverTime_Charge is flagged
10. HITL Charge Current is 2A +/- 30mA | | Estimated Duration | 64 minutes | | Note | ?? |

pytestmark = [Mark(name='parametrize', args=('reset_test_environment', [{'volts': 3.7, 'temperature': 23}]), kwargs={'indirect': True})]
class TestColdTemperatureCharging(hitl_tester.modules.bms.test_handler.CSVRecordEvent):
3779class TestColdTemperatureCharging(CSVRecordEvent):
3780    """Run a test for cold charging."""
3781
3782    def test_cold_temperature_charging(self):
3783        """
3784        | Description          | Cold temperature charging                                              |
3785        | :------------------- | :--------------------------------------------------------------------- |
3786        | GitHub Issue         | turnaroundfactor/HITL#609                                              |
3787        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3788jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D42)                          |
3789        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
3790                                 2. Put cells in rested state at 3.7V per cell                     </br>\
3791                                 4. Attempt to charge at 3.2A                                      </br>\
3792                                 6. Set THERM1 and THERM2 to 0°C                                   </br>\
3793                                 7. Attempt to charge at 3.2A                                      </br>\
3794                                 7. Wait 5 seconds                                                 </br>\
3795                                 7. Disable charging                                               </br>\
3796                                 7. Wait 65 seconds                                                </br>\
3797                                 7. Set THERM1 and THERM2 to 7°C                                   </br>\
3798                                 8. Attempt to charge at 3.2A                                           |
3799        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 23°C +/- 1.1°C             </br>\
3800                                 ⦁ Expect HITL Terminal Current to be 3.2A +/- 30mA                </br>\
3801                                 ⦁ Expect Serial THERM1 & THERM 2 to be 0°C +/- 1.1°C              </br>\
3802                                 ⦁ Expect HITL Terminal Current to be 0A +/- 30mA                  </br>\
3803                                 ⦁ Expect Serial THERM1 & THERM 2 to be 7°C +/- 1.1°C              </br>\
3804                                 ⦁ Expect HITL Terminal Current to be 3.2A +/- 30mA                     |
3805        | Estimated Duration   | 17 seconds                                                             |
3806        """
3807
3808        failed_tests = []
3809        temperatures = [23, 0, 7]
3810
3811        for set_temp in temperatures:
3812            logger.write_info_to_report(f"Setting THERM1 & THERM2 to {set_temp}°C")
3813
3814            _plateset.disengage_safety_protocols = True
3815            _plateset.thermistor1 = _plateset.thermistor2 = set_temp
3816            _plateset.disengage_safety_protocols = False
3817
3818            time.sleep(2)
3819
3820            # Get the serial data
3821            serial_data = serial_monitor.read()
3822
3823            # Convert temperature to Celsius from Kelvin
3824            therm_one = serial_data["dk_temp"] / 10 - 273
3825            therm_two = serial_data["dk_temp1"] / 10 - 273
3826            temp_range = f"{set_temp}°C +/- 1.1°C"
3827            low_range = set_temp - 1.1
3828            high_range = set_temp + 1.1
3829
3830            if low_range <= therm_one <= high_range:
3831                logger.write_result_to_html_report(
3832                    f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
3833                )
3834            else:
3835                logger.write_result_to_html_report(
3836                    f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
3837                    f"of {temp_range}</font>"
3838                )
3839                failed_tests.append("THERM1")
3840
3841            if low_range <= therm_two <= high_range:
3842                logger.write_result_to_html_report(
3843                    f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
3844                )
3845            else:
3846                logger.write_result_to_html_report(
3847                    f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
3848                    f"expected range of {temp_range}</font>"
3849                )
3850                failed_tests.append("THERM2")
3851
3852            logger.write_info_to_report("Attempting to charge at 1A")
3853            limit = 0.030
3854            charge_current = 3.2
3855            expected_current_range = f"3.2A +/- {limit}A"
3856            with _bms.charger(16.8, charge_current):
3857                time.sleep(1)
3858                if set_temp == 0:
3859                    expected_current_range = f"0A +/- {limit}A"
3860                    charge_current = 0
3861                charger_amps = _bms.charger.amps
3862                if charge_current - limit <= charger_amps <= charge_current + limit:
3863                    logger.write_result_to_html_report(
3864                        f"HITL Terminal Current was {charger_amps:.3f}A after charging, which was within the "
3865                        f"expected range of {expected_current_range}"
3866                    )
3867                else:
3868                    logger.write_result_to_html_report(
3869                        f'<font color="#990000">HITL Terminal Current was {charger_amps:.3f}A after charging, '
3870                        f"which was not within the expected range of {expected_current_range} </font>"
3871                    )
3872                    failed_tests.append("HITL Terminal Current")
3873
3874                if set_temp == 0:
3875                    time.sleep(5)
3876            if set_temp == 0:
3877                time.sleep(65)
3878
3879        if len(failed_tests) > 0:
3880            pytest.fail()
3881
3882        logger.write_result_to_html_report("All checks passed test")

Run a test for cold charging.

def test_cold_temperature_charging(self):
3782    def test_cold_temperature_charging(self):
3783        """
3784        | Description          | Cold temperature charging                                              |
3785        | :------------------- | :--------------------------------------------------------------------- |
3786        | GitHub Issue         | turnaroundfactor/HITL#609                                              |
3787        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3788jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D42)                          |
3789        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
3790                                 2. Put cells in rested state at 3.7V per cell                     </br>\
3791                                 4. Attempt to charge at 3.2A                                      </br>\
3792                                 6. Set THERM1 and THERM2 to 0°C                                   </br>\
3793                                 7. Attempt to charge at 3.2A                                      </br>\
3794                                 7. Wait 5 seconds                                                 </br>\
3795                                 7. Disable charging                                               </br>\
3796                                 7. Wait 65 seconds                                                </br>\
3797                                 7. Set THERM1 and THERM2 to 7°C                                   </br>\
3798                                 8. Attempt to charge at 3.2A                                           |
3799        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 23°C +/- 1.1°C             </br>\
3800                                 ⦁ Expect HITL Terminal Current to be 3.2A +/- 30mA                </br>\
3801                                 ⦁ Expect Serial THERM1 & THERM 2 to be 0°C +/- 1.1°C              </br>\
3802                                 ⦁ Expect HITL Terminal Current to be 0A +/- 30mA                  </br>\
3803                                 ⦁ Expect Serial THERM1 & THERM 2 to be 7°C +/- 1.1°C              </br>\
3804                                 ⦁ Expect HITL Terminal Current to be 3.2A +/- 30mA                     |
3805        | Estimated Duration   | 17 seconds                                                             |
3806        """
3807
3808        failed_tests = []
3809        temperatures = [23, 0, 7]
3810
3811        for set_temp in temperatures:
3812            logger.write_info_to_report(f"Setting THERM1 & THERM2 to {set_temp}°C")
3813
3814            _plateset.disengage_safety_protocols = True
3815            _plateset.thermistor1 = _plateset.thermistor2 = set_temp
3816            _plateset.disengage_safety_protocols = False
3817
3818            time.sleep(2)
3819
3820            # Get the serial data
3821            serial_data = serial_monitor.read()
3822
3823            # Convert temperature to Celsius from Kelvin
3824            therm_one = serial_data["dk_temp"] / 10 - 273
3825            therm_two = serial_data["dk_temp1"] / 10 - 273
3826            temp_range = f"{set_temp}°C +/- 1.1°C"
3827            low_range = set_temp - 1.1
3828            high_range = set_temp + 1.1
3829
3830            if low_range <= therm_one <= high_range:
3831                logger.write_result_to_html_report(
3832                    f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
3833                )
3834            else:
3835                logger.write_result_to_html_report(
3836                    f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
3837                    f"of {temp_range}</font>"
3838                )
3839                failed_tests.append("THERM1")
3840
3841            if low_range <= therm_two <= high_range:
3842                logger.write_result_to_html_report(
3843                    f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
3844                )
3845            else:
3846                logger.write_result_to_html_report(
3847                    f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
3848                    f"expected range of {temp_range}</font>"
3849                )
3850                failed_tests.append("THERM2")
3851
3852            logger.write_info_to_report("Attempting to charge at 1A")
3853            limit = 0.030
3854            charge_current = 3.2
3855            expected_current_range = f"3.2A +/- {limit}A"
3856            with _bms.charger(16.8, charge_current):
3857                time.sleep(1)
3858                if set_temp == 0:
3859                    expected_current_range = f"0A +/- {limit}A"
3860                    charge_current = 0
3861                charger_amps = _bms.charger.amps
3862                if charge_current - limit <= charger_amps <= charge_current + limit:
3863                    logger.write_result_to_html_report(
3864                        f"HITL Terminal Current was {charger_amps:.3f}A after charging, which was within the "
3865                        f"expected range of {expected_current_range}"
3866                    )
3867                else:
3868                    logger.write_result_to_html_report(
3869                        f'<font color="#990000">HITL Terminal Current was {charger_amps:.3f}A after charging, '
3870                        f"which was not within the expected range of {expected_current_range} </font>"
3871                    )
3872                    failed_tests.append("HITL Terminal Current")
3873
3874                if set_temp == 0:
3875                    time.sleep(5)
3876            if set_temp == 0:
3877                time.sleep(65)
3878
3879        if len(failed_tests) > 0:
3880            pytest.fail()
3881
3882        logger.write_result_to_html_report("All checks passed test")
Description Cold temperature charging
GitHub Issue turnaroundfactor/HITL#609
Google Docs Google Sheet Cell
Instructions 1. Set THERM1 and THERM2 to 23°C
2. Put cells in rested state at 3.7V per cell
4. Attempt to charge at 3.2A
6. Set THERM1 and THERM2 to 0°C
7. Attempt to charge at 3.2A
7. Wait 5 seconds
7. Disable charging
7. Wait 65 seconds
7. Set THERM1 and THERM2 to 7°C
8. Attempt to charge at 3.2A
Pass / Fail Criteria ⦁ Expect Serial THERM1 & THERM 2 to be 23°C +/- 1.1°C
⦁ Expect HITL Terminal Current to be 3.2A +/- 30mA
⦁ Expect Serial THERM1 & THERM 2 to be 0°C +/- 1.1°C
⦁ Expect HITL Terminal Current to be 0A +/- 30mA
⦁ Expect Serial THERM1 & THERM 2 to be 7°C +/- 1.1°C
⦁ Expect HITL Terminal Current to be 3.2A +/- 30mA
Estimated Duration 17 seconds
class TestColdTemperatureCurrent(hitl_tester.modules.bms.test_handler.CSVRecordEvent):
3885class TestColdTemperatureCurrent(CSVRecordEvent):
3886    """Run a test for cold current."""
3887
3888    def set_temperature(self, celsius: float) -> bool:
3889        """Set and check the temperature."""
3890        test_failed = False
3891
3892        logger.write_info_to_report(f"Setting THERM1 & THERM2 to {celsius}°C")
3893
3894        _plateset.disengage_safety_protocols = True
3895        _plateset.thermistor1 = _plateset.thermistor2 = celsius
3896        _plateset.disengage_safety_protocols = False
3897
3898        time.sleep(2)
3899
3900        # Get the serial data
3901        serial_data = serial_monitor.read()
3902        assert serial_data, "No serial data recieved."
3903
3904        # Convert temperature to Celsius from Kelvin
3905        therm_one = int(serial_data["dk_temp"]) / 10 - 273
3906        therm_two = int(serial_data["dk_temp1"]) / 10 - 273
3907        temp_range = f"{celsius}°C +/- 1.1°C"
3908        low_range = celsius - 1.1
3909        high_range = celsius + 1.1
3910
3911        if low_range <= therm_one <= high_range:
3912            logger.write_result_to_html_report(
3913                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
3914            )
3915        else:
3916            logger.write_result_to_html_report(
3917                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
3918                f"of {temp_range}</font>"
3919            )
3920            test_failed = True
3921
3922        if low_range <= therm_two <= high_range:
3923            logger.write_result_to_html_report(
3924                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
3925            )
3926        else:
3927            logger.write_result_to_html_report(
3928                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
3929                f"expected range of {temp_range}</font>"
3930            )
3931            test_failed = True
3932        return test_failed
3933
3934    def test_cold_temperature_current(self):
3935        """
3936        | Description          | Cold temperature current                                               |
3937        | :------------------- | :--------------------------------------------------------------------- |
3938        | GitHub Issue         | turnaroundfactor/HITL#609                                              |
3939        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3940jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D43)                          |
3941        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
3942                                 2. Put cells in rested state at 3.7V per cell                     </br>\
3943                                 3. Read SMBus charging current                                    </br>\
3944                                 4. Set THERM1 and THERM2 to 0°C                                   </br>\
3945                                 5. Read SMBus charging current                                         |
3946        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 23°C +/- 1.1°C             </br>\
3947                                 ⦁ Expect SMBus charging current 0x07D0 (2000 mA)                  </br>\
3948                                 ⦁ Expect Serial THERM1 & THERM 2 to be 0°C +/- 1.1°C              </br>\
3949                                 ⦁ Expect SMBus charging current 0x03EB (1000 mA)                       |
3950        | Estimated Duration   | 17 seconds                                                             |
3951        | Note                 | 0x07D0  (Note: This is the default value for debug build, Production   \
3952                                 build should return 0x1770)                                            |
3953        """
3954
3955        for temperature, expected_value in ((23, 2000), (0, 1000)):
3956            test_failed = self.set_temperature(temperature)
3957            time.sleep(5)
3958            charging_current = _smbus.read_register(SMBusReg.CHARGING_CURRENT)
3959            if charging_current[0] == expected_value:
3960                logger.write_result_to_html_report(f"Charging current: {charging_current[0]} = {expected_value}")
3961            else:
3962                logger.write_failure_to_html_report(f"Charging current: {charging_current[0]}{expected_value}")
3963                test_failed = True
3964
3965        if test_failed:
3966            pytest.fail()

Run a test for cold current.

def set_temperature(self, celsius: float) -> bool:
3888    def set_temperature(self, celsius: float) -> bool:
3889        """Set and check the temperature."""
3890        test_failed = False
3891
3892        logger.write_info_to_report(f"Setting THERM1 & THERM2 to {celsius}°C")
3893
3894        _plateset.disengage_safety_protocols = True
3895        _plateset.thermistor1 = _plateset.thermistor2 = celsius
3896        _plateset.disengage_safety_protocols = False
3897
3898        time.sleep(2)
3899
3900        # Get the serial data
3901        serial_data = serial_monitor.read()
3902        assert serial_data, "No serial data recieved."
3903
3904        # Convert temperature to Celsius from Kelvin
3905        therm_one = int(serial_data["dk_temp"]) / 10 - 273
3906        therm_two = int(serial_data["dk_temp1"]) / 10 - 273
3907        temp_range = f"{celsius}°C +/- 1.1°C"
3908        low_range = celsius - 1.1
3909        high_range = celsius + 1.1
3910
3911        if low_range <= therm_one <= high_range:
3912            logger.write_result_to_html_report(
3913                f"THERM1 was {therm_one:.1f}°C, which was within the expected temperature range of {temp_range}"
3914            )
3915        else:
3916            logger.write_result_to_html_report(
3917                f'<font color="#990000">THERM1 was {therm_one:.1f}°C, which was not within the expected range '
3918                f"of {temp_range}</font>"
3919            )
3920            test_failed = True
3921
3922        if low_range <= therm_two <= high_range:
3923            logger.write_result_to_html_report(
3924                f"THERM2 was {therm_two:.1f}°C, which was within the expected temperature range of {temp_range}"
3925            )
3926        else:
3927            logger.write_result_to_html_report(
3928                f'<font color="#990000">THERM2 was {therm_one:.1f}°C, which was not within the '
3929                f"expected range of {temp_range}</font>"
3930            )
3931            test_failed = True
3932        return test_failed

Set and check the temperature.

def test_cold_temperature_current(self):
3934    def test_cold_temperature_current(self):
3935        """
3936        | Description          | Cold temperature current                                               |
3937        | :------------------- | :--------------------------------------------------------------------- |
3938        | GitHub Issue         | turnaroundfactor/HITL#609                                              |
3939        | Google Docs          | [Google Sheet Cell](https://docs.google.com/spreadsheets/d/1r5A-g2twpNj\
3940jZx_BjjZk90XgvQdicU9yoG9foicz_Nk/edit?gid=2093042698#gid=2093042698&range=D43)                          |
3941        | Instructions         | 1. Set THERM1 and THERM2 to 23°C                                  </br>\
3942                                 2. Put cells in rested state at 3.7V per cell                     </br>\
3943                                 3. Read SMBus charging current                                    </br>\
3944                                 4. Set THERM1 and THERM2 to 0°C                                   </br>\
3945                                 5. Read SMBus charging current                                         |
3946        | Pass / Fail Criteria | ⦁ Expect Serial THERM1 & THERM 2 to be 23°C +/- 1.1°C             </br>\
3947                                 ⦁ Expect SMBus charging current 0x07D0 (2000 mA)                  </br>\
3948                                 ⦁ Expect Serial THERM1 & THERM 2 to be 0°C +/- 1.1°C              </br>\
3949                                 ⦁ Expect SMBus charging current 0x03EB (1000 mA)                       |
3950        | Estimated Duration   | 17 seconds                                                             |
3951        | Note                 | 0x07D0  (Note: This is the default value for debug build, Production   \
3952                                 build should return 0x1770)                                            |
3953        """
3954
3955        for temperature, expected_value in ((23, 2000), (0, 1000)):
3956            test_failed = self.set_temperature(temperature)
3957            time.sleep(5)
3958            charging_current = _smbus.read_register(SMBusReg.CHARGING_CURRENT)
3959            if charging_current[0] == expected_value:
3960                logger.write_result_to_html_report(f"Charging current: {charging_current[0]} = {expected_value}")
3961            else:
3962                logger.write_failure_to_html_report(f"Charging current: {charging_current[0]}{expected_value}")
3963                test_failed = True
3964
3965        if test_failed:
3966            pytest.fail()
Description Cold temperature current
GitHub Issue turnaroundfactor/HITL#609
Google Docs Google Sheet Cell
Instructions 1. Set THERM1 and THERM2 to 23°C
2. Put cells in rested state at 3.7V per cell
3. Read SMBus charging current
4. Set THERM1 and THERM2 to 0°C
5. Read SMBus charging current
Pass / Fail Criteria ⦁ Expect Serial THERM1 & THERM 2 to be 23°C +/- 1.1°C
⦁ Expect SMBus charging current 0x07D0 (2000 mA)
⦁ Expect Serial THERM1 & THERM 2 to be 0°C +/- 1.1°C
⦁ Expect SMBus charging current 0x03EB (1000 mA)
Estimated Duration 17 seconds
Note 0x07D0 (Note: This is the default value for debug build, Production build should return 0x1770)