hitl_tester.test_cases.bms.smart_charger_cycles

Test Smart Charger
GitHub Issue(s) turnaroundfactor/HITL#314
Description Tests that our BB2590 works on a smart charger

Used in these test plans:

  • smart_charger_cycles_b ⠀⠀⠀(bms/smart_charger_cycles_b.plan)
  • smart_charger_cycles_a ⠀⠀⠀(bms/smart_charger_cycles_a.plan)

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

  • ./hitl_tester.py smart_charger_cycles_b -DSAMPLE_TIME=10 -DEXPECTED_CURRENT=2.0 -DFAILED={'current': [0, 0], 'soc': [0, 0], 'cycle': [0, 0]}
  1"""
  2| Test                 | Smart Charger                                                       |
  3| :------------------- | :------------------------------------------------------------------ |
  4| GitHub Issue(s)      | turnaroundfactor/HITL#314                                    |
  5| Description          | Tests that our BB2590 works on a smart charger                      |
  6"""
  7
  8from __future__ import annotations
  9
 10import time
 11from contextlib import contextmanager
 12
 13import pytest
 14
 15from hitl_tester.modules.bms.bms_hw import BMSHardware
 16from hitl_tester.modules.bms.bms_serial import serial_monitor
 17from hitl_tester.modules.bms.i2c_driver import I2CFileSniffer
 18from hitl_tester.modules.bms.plateset import Plateset
 19from hitl_tester.modules.logger import logger
 20
 21SAMPLE_TIME = 10
 22EXPECTED_CURRENT = 2.0
 23
 24_bms = BMSHardware(pytest.flags)  # type: ignore[arg-type]
 25_bms.init()
 26_plateset = Plateset()
 27_i2c_file_sniffer = I2CFileSniffer()
 28
 29FAILED = {"current": [0, 0], "soc": [0, 0], "cycle": [0, 0]}
 30
 31
 32class FailureLog:
 33    """Monitor how successful a test is from 0 (pass) to 1 (fail)."""
 34
 35    def __init__(self, name: str):
 36        self.name = name
 37        self.failing_average = 0.0
 38        self.samples = 0
 39
 40    def update(self, failed: bool):
 41        """Update with fail of pass."""
 42        self.failing_average = (failed + self.samples * self.failing_average) / (self.samples + 1)
 43        self.samples += 1
 44
 45    def failed(self) -> bool:
 46        """Log failure."""
 47        logger.write_warning_to_report(f"{self.name} test failed for {self.failing_average:%} of test.")
 48        return self.failing_average != 0.0
 49
 50
 51current_log = FailureLog("Current")
 52soc_log = FailureLog("SOC")
 53cycle_log = FailureLog("Cycle")
 54
 55
 56def log_data(start_time: float, serial_data: dict[str, int | bool | str]):
 57    """Log data while charging/discharging."""
 58    elapsed_time = time.perf_counter() - start_time
 59    _bms.csv.cycle_smbus.record(elapsed_time, suppress_smbus=True)
 60    voltages = [f"{cell.measured_volts:.2f}V" for cell in _bms.cells.values()]
 61    cell_socs = [cell.state_of_charge for cell in _bms.cells.values()]
 62    cell_socs_str = ", ".join(map(lambda soc: f"{soc:%}", cell_socs))
 63    serial_amps = float(serial_data["mamps"]) / 1000
 64    serial_soc = float(serial_data["percent_charged"]) / 100
 65    logger.write_info_to_report(
 66        f"Elapsed Time: {elapsed_time}, Cell Voltages: {', '.join(voltages)}, "
 67        f"Cell SOCs: {cell_socs_str}, Serial SOC: {serial_soc}, Serial Amps: {serial_amps}"
 68    )
 69
 70    # Check Current
 71    amps_error = abs(EXPECTED_CURRENT - serial_amps)
 72    if failed := amps_error > 0.100:
 73        logger.write_warning_to_report(
 74            f"Current error of {amps_error * 1000} mA is more than 100 mA. "
 75            f"Current was {serial_amps} A (expected {EXPECTED_CURRENT} A)"
 76        )
 77    current_log.update(failed)
 78
 79    # Check SOC
 80    soc_error = max(abs(cell_soc - serial_soc) for cell_soc in cell_socs)
 81    if failed := soc_error > 0.05:
 82        logger.write_warning_to_report(
 83            f"SOC error of {soc_error:%} is more than 5%. SOC was {serial_soc:%} (expected {cell_socs_str})"
 84        )
 85    soc_log.update(failed)
 86
 87    time.sleep(SAMPLE_TIME)
 88
 89
 90@contextmanager
 91def managed_charger_relay():
 92    """Context manager for charger relay."""
 93    _plateset.charger_switch = True
 94    logger.write_info_to_report("Charge relay enabled, waiting 2 minutes...")
 95    time.sleep(120)
 96    try:
 97        yield
 98    finally:
 99        _plateset.charger_switch = False
100        logger.write_info_to_report("Charge relay disabled")
101
102
103@pytest.mark.sim_cells
104def test_smart_charger():
105    """
106    | Requirement          | Smart Charger Cycles                                                                     |
107    | :------------------- | :--------------------------------------------------------------------------------------- |
108    | GitHub Issue(s)      | turnaroundfactor/HITL#467                                                         |
109    | Instructions         | 1. Charge relays on until fully charged (0A)                                        </br>\
110                             2. Discharge relays on until fully discharged at 10V (electronic load)              </br>\
111                             3. Repeat 10 times total                                                                 |
112    | Pass / Fail Criteria | Charge cycle increments, SOC matches cell sims, charge current is 1-2A (depends on test) |
113    """
114
115    logger.write_info_to_report("Sleeping 10 seconds...")
116    time.sleep(10)
117
118    start_time = time.perf_counter()
119    _i2c_file_sniffer.start(start_time)  # Begin scanning i2c to csv
120    serial_data = serial_monitor.read(latest=True)
121    starting_cycles = serial_data["charge_cycles"]
122    for cycle in range(1, 11):
123        # Charge
124        with managed_charger_relay():
125            while serial_data["mamps"] > 100:
126                log_data(start_time, serial_data)
127                serial_data = serial_monitor.read(latest=True)
128
129        # Discharge
130        with _bms.load(2.0):
131            while serial_data["mvolt_terminal"] > 10_000:
132                log_data(start_time, serial_data)
133                serial_data = serial_monitor.read(latest=True)
134
135        # Check Charge Cycles
136        if failed := serial_data["charge_cycles"] != starting_cycles + cycle:
137            logger.write_warning_to_report(
138                f"Charge cycle was {serial_data['charge_cycles']} (expected {starting_cycles + cycle})"
139            )
140        cycle_log.update(failed)
141
142    if current_log.failed() or soc_log.failed() or cycle_log.failed():
143        pytest.fail("Failed")
SAMPLE_TIME = 10
EXPECTED_CURRENT = 2.0
FAILED = {'current': [0, 0], 'soc': [0, 0], 'cycle': [0, 0]}
class FailureLog:
33class FailureLog:
34    """Monitor how successful a test is from 0 (pass) to 1 (fail)."""
35
36    def __init__(self, name: str):
37        self.name = name
38        self.failing_average = 0.0
39        self.samples = 0
40
41    def update(self, failed: bool):
42        """Update with fail of pass."""
43        self.failing_average = (failed + self.samples * self.failing_average) / (self.samples + 1)
44        self.samples += 1
45
46    def failed(self) -> bool:
47        """Log failure."""
48        logger.write_warning_to_report(f"{self.name} test failed for {self.failing_average:%} of test.")
49        return self.failing_average != 0.0

Monitor how successful a test is from 0 (pass) to 1 (fail).

FailureLog(name: str)
36    def __init__(self, name: str):
37        self.name = name
38        self.failing_average = 0.0
39        self.samples = 0
name
failing_average
samples
def update(self, failed: bool):
41    def update(self, failed: bool):
42        """Update with fail of pass."""
43        self.failing_average = (failed + self.samples * self.failing_average) / (self.samples + 1)
44        self.samples += 1

Update with fail of pass.

def failed(self) -> bool:
46    def failed(self) -> bool:
47        """Log failure."""
48        logger.write_warning_to_report(f"{self.name} test failed for {self.failing_average:%} of test.")
49        return self.failing_average != 0.0

Log failure.

current_log = <FailureLog object>
soc_log = <FailureLog object>
cycle_log = <FailureLog object>
def log_data(start_time: float, serial_data: dict[str, int | bool | str]):
57def log_data(start_time: float, serial_data: dict[str, int | bool | str]):
58    """Log data while charging/discharging."""
59    elapsed_time = time.perf_counter() - start_time
60    _bms.csv.cycle_smbus.record(elapsed_time, suppress_smbus=True)
61    voltages = [f"{cell.measured_volts:.2f}V" for cell in _bms.cells.values()]
62    cell_socs = [cell.state_of_charge for cell in _bms.cells.values()]
63    cell_socs_str = ", ".join(map(lambda soc: f"{soc:%}", cell_socs))
64    serial_amps = float(serial_data["mamps"]) / 1000
65    serial_soc = float(serial_data["percent_charged"]) / 100
66    logger.write_info_to_report(
67        f"Elapsed Time: {elapsed_time}, Cell Voltages: {', '.join(voltages)}, "
68        f"Cell SOCs: {cell_socs_str}, Serial SOC: {serial_soc}, Serial Amps: {serial_amps}"
69    )
70
71    # Check Current
72    amps_error = abs(EXPECTED_CURRENT - serial_amps)
73    if failed := amps_error > 0.100:
74        logger.write_warning_to_report(
75            f"Current error of {amps_error * 1000} mA is more than 100 mA. "
76            f"Current was {serial_amps} A (expected {EXPECTED_CURRENT} A)"
77        )
78    current_log.update(failed)
79
80    # Check SOC
81    soc_error = max(abs(cell_soc - serial_soc) for cell_soc in cell_socs)
82    if failed := soc_error > 0.05:
83        logger.write_warning_to_report(
84            f"SOC error of {soc_error:%} is more than 5%. SOC was {serial_soc:%} (expected {cell_socs_str})"
85        )
86    soc_log.update(failed)
87
88    time.sleep(SAMPLE_TIME)

Log data while charging/discharging.

@contextmanager
def managed_charger_relay():
 91@contextmanager
 92def managed_charger_relay():
 93    """Context manager for charger relay."""
 94    _plateset.charger_switch = True
 95    logger.write_info_to_report("Charge relay enabled, waiting 2 minutes...")
 96    time.sleep(120)
 97    try:
 98        yield
 99    finally:
100        _plateset.charger_switch = False
101        logger.write_info_to_report("Charge relay disabled")

Context manager for charger relay.

@pytest.mark.sim_cells
def test_smart_charger():
104@pytest.mark.sim_cells
105def test_smart_charger():
106    """
107    | Requirement          | Smart Charger Cycles                                                                     |
108    | :------------------- | :--------------------------------------------------------------------------------------- |
109    | GitHub Issue(s)      | turnaroundfactor/HITL#467                                                         |
110    | Instructions         | 1. Charge relays on until fully charged (0A)                                        </br>\
111                             2. Discharge relays on until fully discharged at 10V (electronic load)              </br>\
112                             3. Repeat 10 times total                                                                 |
113    | Pass / Fail Criteria | Charge cycle increments, SOC matches cell sims, charge current is 1-2A (depends on test) |
114    """
115
116    logger.write_info_to_report("Sleeping 10 seconds...")
117    time.sleep(10)
118
119    start_time = time.perf_counter()
120    _i2c_file_sniffer.start(start_time)  # Begin scanning i2c to csv
121    serial_data = serial_monitor.read(latest=True)
122    starting_cycles = serial_data["charge_cycles"]
123    for cycle in range(1, 11):
124        # Charge
125        with managed_charger_relay():
126            while serial_data["mamps"] > 100:
127                log_data(start_time, serial_data)
128                serial_data = serial_monitor.read(latest=True)
129
130        # Discharge
131        with _bms.load(2.0):
132            while serial_data["mvolt_terminal"] > 10_000:
133                log_data(start_time, serial_data)
134                serial_data = serial_monitor.read(latest=True)
135
136        # Check Charge Cycles
137        if failed := serial_data["charge_cycles"] != starting_cycles + cycle:
138            logger.write_warning_to_report(
139                f"Charge cycle was {serial_data['charge_cycles']} (expected {starting_cycles + cycle})"
140            )
141        cycle_log.update(failed)
142
143    if current_log.failed() or soc_log.failed() or cycle_log.failed():
144        pytest.fail("Failed")
Requirement Smart Charger Cycles
GitHub Issue(s) turnaroundfactor/HITL#467
Instructions 1. Charge relays on until fully charged (0A)
2. Discharge relays on until fully discharged at 10V (electronic load)
3. Repeat 10 times total
Pass / Fail Criteria Charge cycle increments, SOC matches cell sims, charge current is 1-2A (depends on test)