hitl_tester.test_cases.bms.test_2590_cell_sims

Modified version of the tests in test_2590_live.py, for use by cell simulators.

Used in these test plans:

  • new_2590_test ⠀⠀⠀(bms/new_2590_test.plan)
  • milprf_dev_b ⠀⠀⠀(bms/milprf_dev_b.plan)
  • milprf_dev_a ⠀⠀⠀(bms/milprf_dev_a.plan)

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

  • ./hitl_tester.py new_2590_test -DMAX_TIME=30 -DCELL_VOLTAGE=1.375 -DDEFAULT_SOC_PERCENT=0.8 -DDEFAULT_TEMPERATURE_C=15 -DREST_TIME=10800 -DREST_INTERVAL=10 -DDISCHARGE_VOLTAGE=10 -DDISCHARGE_CURRENT=2 -DMINIMUM_READINGS=3 -DCHARGE_INC_START=0 -DCHARGE_INC_STOP=3 -DCHARGE_INC_STEP=0.02 -DCHARGE_INC_TIME=20 -DSAMPLE_INTERVAL=10 -DWAKEUP_CURRENT=0.01 -DCYCLE_COUNT=6 -DCYCLE_CAPACITY=0.6
  1"""
  2Modified version of the tests in test_2590_live.py, for use by cell simulators.
  3"""
  4
  5from __future__ import annotations
  6
  7import time
  8
  9import pytest
 10
 11from hitl_tester.modules.bms.bms_hw import BMSHardware
 12from hitl_tester.modules.bms.event_watcher import SerialWatcher
 13from hitl_tester.modules.bms.plateset import Plateset
 14from hitl_tester.modules.bms_types import (
 15    OverVoltageError,
 16    TimeoutExceededError,
 17    UnderVoltageError,
 18    DischargeType,
 19    StopWatch,
 20)
 21from hitl_tester.modules.logger import logger
 22
 23MAX_TIME = 30
 24CELL_VOLTAGE = 1.375
 25
 26DEFAULT_SOC_PERCENT = 0.80
 27DEFAULT_TEMPERATURE_C = 15
 28
 29REST_TIME = 3 * 3600
 30REST_INTERVAL = 10
 31
 32DISCHARGE_VOLTAGE = 10
 33DISCHARGE_CURRENT = 2
 34
 35MINIMUM_READINGS = 3
 36CHARGE_INC_START = 0
 37CHARGE_INC_STOP = 3
 38CHARGE_INC_STEP = 0.020
 39CHARGE_INC_TIME = 20
 40SAMPLE_INTERVAL = 10
 41
 42WAKEUP_CURRENT = 0.010
 43
 44CYCLE_COUNT = 6
 45CYCLE_CAPACITY = 0.6
 46
 47bms_hardware = BMSHardware(pytest.flags)  # type: ignore[arg-type]
 48bms_hardware.init()
 49
 50serial_watcher = SerialWatcher()
 51plateset = Plateset()
 52
 53
 54def test_reset_cell_sims():
 55    """Activate cell sims and set appropriate temperatures."""
 56    logger.write_info_to_report("Powering down cell sims")
 57    for cell in bms_hardware.cells.values():
 58        cell.disengage_safety_protocols = True
 59        cell.volts = 0.0001
 60    time.sleep(5)
 61    logger.write_info_to_report("Powering up cell sims")
 62    for cell in bms_hardware.cells.values():
 63        cell.state_of_charge = DEFAULT_SOC_PERCENT
 64        cell.disengage_safety_protocols = False
 65    logger.write_info_to_report(f"Setting temperature to {DEFAULT_TEMPERATURE_C}°C")
 66    plateset.thermistor1 = DEFAULT_TEMPERATURE_C
 67    plateset.thermistor2 = DEFAULT_TEMPERATURE_C
 68
 69
 70def check_cell_imbalance_conditions(high_cell_id: int):
 71    """Set one cell 100mV above the rest, confirm its flag is set, and that it has a higher current."""
 72    serial_watcher.assert_true(f"flags.output_c{high_cell_id}_bal", False)
 73    old_cell_current = bms_hardware.cells[high_cell_id].amps  # NOTE: Do I need to be (dis)charging?
 74    for i, cell in bms_hardware.cells.items():
 75        logger.write_debug_to_report(f"Cell {i}: {cell.measured_volts} V ({cell.volts}), {cell.amps} A")
 76
 77    logger.write_info_to_report(f"Cell {high_cell_id} = 4.0 V")
 78    bms_hardware.cells[high_cell_id].exact_volts = 4.0
 79    for cell_id in bms_hardware.cells:
 80        serial_watcher.assert_true(f"flags.output_c{cell_id}_bal", cell_id == high_cell_id)
 81
 82    # We should see the current increase when a flag is set. The current will be cell voltage / 100
 83    cell_current = bms_hardware.cells[high_cell_id].amps
 84    for i, cell in bms_hardware.cells.items():
 85        logger.write_debug_to_report(f"Cell {i}: {cell.measured_volts} V ({cell.volts}), {cell.amps} A")
 86    logger.write_info_to_report(f"{cell_current} A > {old_cell_current} A?")
 87    assert cell_current > old_cell_current
 88    new_cell_current = cell_current - old_cell_current
 89    expected_current_a = bms_hardware.cells[high_cell_id].volts / 100
 90    logger.write_info_to_report(f"{new_cell_current} A ≈ {expected_current_a} A")
 91    current_error_a = 0.02
 92    assert expected_current_a - current_error_a <= new_cell_current <= expected_current_a + current_error_a
 93
 94    # Reset the higher voltage
 95    logger.write_info_to_report(f"Cell {high_cell_id} = 3.9 V")
 96    bms_hardware.cells[high_cell_id].exact_volts = 3.9
 97    for i, cell in bms_hardware.cells.items():
 98        logger.write_debug_to_report(f"Cell {i}: {cell.measured_volts} V (target: {cell.volts}), {cell.amps} A")
 99    for cell_id in bms_hardware.cells:
100        serial_watcher.assert_true(f"flags.output_c{cell_id}_bal", False)
101
102
103def test_cell_imbalance():
104    """
105    The flag is set when a cell is 4.2V+ or 20mv above the lowest cell.
106
107    What that is actually doing though is connecting a resistor across the cell that is too high in voltage.
108    If you monitor the cell simulator current you should actually see the current increase in that cell if
109    the flag get set. The current would be Vcell / 100 since they use 100 Ohm resistors.
110    """
111    logger.write_info_to_report("Testing Cell Imbalance")
112    serial_watcher.start()
113    for cell in bms_hardware.cells.values():
114        cell.exact_volts = 3.9
115    for cell_id in bms_hardware.cells:
116        serial_watcher.assert_true(f"flags.output_c{cell_id}_bal", False)
117
118    # Resting
119    logger.write_info_to_report("Resting")
120    for cell_id in bms_hardware.cells:
121        check_cell_imbalance_conditions(cell_id)
122
123    # Discharging
124    logger.write_info_to_report("Discharging at 100 mA")
125    bms_hardware.load.mode_cc()
126    bms_hardware.load.amps = 0.100
127    bms_hardware.load.enable()
128    for cell_id in bms_hardware.cells:
129        check_cell_imbalance_conditions(cell_id)
130    bms_hardware.load.disable()
131
132    # Charging
133    logger.write_info_to_report("Charging at 100 mA")
134    bms_hardware.charger.set_profile(3.9 * 4, 0.1)
135    plateset.ce_switch = True
136    bms_hardware.charger.enable()
137    for cell_id in bms_hardware.cells:
138        check_cell_imbalance_conditions(cell_id)
139    bms_hardware.charger.disable()
140    plateset.ce_switch = False
141
142    serial_watcher.stop()
143
144
145def test_charge_enable():
146    """
147    Confirm we can charge in excess of 400 mA when charge enable is on.
148    If battery is near full charge, you may not be able to go past 400 mA when charging.
149
150    We want to disable the charging current if charge enable is off if the current goes over 400 mA per
151    the spec. In reality, we want to disable if current is over 20 mA since that's what BT does and it's a safer way
152    to charge. Other OTS may have different cutoffs, and the BT one was measured by experiment.
153    """
154    logger.write_info_to_report("Testing Charge Enable")
155    serial_watcher.start()
156
157    logger.write_info_to_report("Charge enable = 1")
158    plateset.ce_switch = True  # Allows current charging in excess of 400 mA
159    serial_watcher.assert_true("ncharge_en_gpio", False, 1)
160    max_time = 60
161    elapsed_time = 0
162    charge_sample_interval = 5
163    termination_current = 0.100
164    current_limit = 0.400 + 0.100  # When to end the test
165    latest_i = termination_current + 0.1
166    ov_protection = 17
167    latest_v = 0
168
169    # Enable charging and timer
170    bms_hardware.charger.set_profile(volts=16.8, amps=3)
171    bms_hardware.charger.enable()  # Enables the output of the charger
172    bms_hardware.timer.reset()  # Keep track of runtime
173
174    # Charge until termination current, current limit, ov_protection, or max_time
175    while current_limit >= latest_i >= termination_current:
176        if latest_v >= ov_protection:
177            raise OverVoltageError(
178                f"Overvoltage protection triggered at {time.strftime('%x %X')}. "
179                f"Voltage {latest_v} is higher than {ov_protection}."
180            )
181        if elapsed_time >= max_time:
182            raise TimeoutExceededError(f"Current of {current_limit}A was not reached after {max_time} seconds")
183
184        elapsed_time = bms_hardware.timer.elapsed_time
185        latest_v = bms_hardware.dmm.volts
186        latest_i = bms_hardware.charger.amps
187        logger.write_info_to_report(f"Elapsed Time: {elapsed_time}, Voltage: {latest_v}, Current: {latest_i}")
188
189        bms_hardware.csv.cycle.record(elapsed_time)
190        time.sleep(charge_sample_interval)
191    bms_hardware.charger.disable()  # Charging is complete, turn off the charger
192
193    assert latest_i > current_limit
194
195    logger.write_info_to_report("Charge enable = 0")
196    plateset.ce_switch = False  # Does not allow current charging in excess of 400 mA or less
197    serial_watcher.assert_true("ncharge_en_gpio", True, 2)
198    max_time = 60
199    elapsed_time = 0
200    charge_sample_interval = 1
201    termination_current = 0.0
202    current_limit = 0.400 + 0.100  # When to end the test
203    charge_current = 0.001
204    latest_i = termination_current + 0.1
205    ov_protection = 17
206    latest_v = 0
207    max_charge_current = 0
208
209    # Enable charging and timer
210    bms_hardware.charger.set_profile(volts=16.8, amps=charge_current)
211    bms_hardware.charger.enable()  # Enables the output of the charger
212    bms_hardware.timer.reset()  # Keep track of runtime
213
214    # Charge until termination current, current limit, ov_protection, or max_time
215    while current_limit >= latest_i >= termination_current:
216        if latest_v >= ov_protection:
217            raise OverVoltageError(
218                f"Overvoltage protection triggered at {time.strftime('%x %X')}. "
219                f"Voltage {latest_v} is higher than {ov_protection}."
220            )
221        if elapsed_time >= max_time:
222            raise TimeoutExceededError(f"Current of {current_limit}A was not reached after {max_time} seconds")
223
224        elapsed_time = bms_hardware.timer.elapsed_time
225        latest_v = bms_hardware.dmm.volts
226        latest_i = bms_hardware.charger.amps
227        max_charge_current = max(max_charge_current, latest_i)
228        logger.write_info_to_report(f"Elapsed Time: {elapsed_time}, Voltage: {latest_v}, Current: {latest_i}")
229
230        bms_hardware.csv.cycle.record(elapsed_time)
231        charge_current += 0.001  # Increase by 1mA per second
232        bms_hardware.charger.set_profile(volts=16.8, amps=charge_current)
233        time.sleep(charge_sample_interval)
234    bms_hardware.charger.disable()  # Charging is complete, turn off the charger
235
236    assert max_charge_current <= 0.400
237
238    serial_watcher.stop()
239    test_reset_cell_sims()
240
241
242def standard_charge(
243    charge_current: float = 2,
244    max_time: int = 3 * 3600,
245    sample_interval: int = 10,
246    minimum_readings: int = 3,
247    termination_current: float = 0.100,
248):
249    """
250    Charge batteries in accordance with 4.3.1 for not greater than three hours.
251    4.3.1 = 23 ± 5°C (73.4°F) ambient pressure/relative humidity, with 2+ hours between charge and discharge.
252    """
253    bms_hardware.voltage = 16.8
254    bms_hardware.ov_protection = bms_hardware.voltage + 0.050  # 50mV above the charging voltage
255    bms_hardware.current = charge_current
256    bms_hardware.termination_current = termination_current  # 100 mA
257    bms_hardware.max_time = max_time
258    bms_hardware.sample_interval = sample_interval
259    bms_hardware.minimum_readings = minimum_readings
260
261    # Run the Charge cycle
262    bms_hardware.run_li_charge_cycle()
263
264
265def standard_discharge(
266    discharge_current: float = DISCHARGE_CURRENT,
267    max_time: int = 8 * 3600,
268    sample_interval: int = 10,
269    discharge_voltage: float = DISCHARGE_VOLTAGE,
270):
271    """Discharge at 2A until 10V."""
272    bms_hardware.voltage = discharge_voltage
273    bms_hardware.uv_protection = bms_hardware.voltage - 0.500  # 500mV below voltage cutoff
274    bms_hardware.current = discharge_current
275    bms_hardware.discharge_type = DischargeType.CONSTANT_CURRENT
276    bms_hardware.max_time = max_time
277    bms_hardware.sample_interval = sample_interval
278
279    # Run the discharge cycle, returning the capacity
280    capacity = bms_hardware.run_discharge_cycle()
281    logger.write_info_to_report(f"Discharge complete, capacity was {capacity * 1000.0} mAh")
282    return capacity
283
284
285def standard_rest(seconds: int = 0, sample_interval: int = 0):
286    """Stabilize the batteries for 2+ hours."""
287    bms_hardware.max_time = seconds or REST_TIME
288    bms_hardware.sample_interval = sample_interval or SAMPLE_INTERVAL
289    bms_hardware.run_resting_cycle()
290
291
292def test_full_discharge():
293    """Perform a discharge from full."""
294    for cell in bms_hardware.cells.values():
295        cell.state_of_charge = 1.00
296    old_cycle_function = bms_hardware.csv.cycle
297    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
298    bms_hardware.csv.cycle.create_file()
299    standard_discharge(sample_interval=10)
300    bms_hardware.csv.cycle = old_cycle_function
301
302
303def test_capacity_discharge():
304    """
305    Discharge at 2A (per Mil-prf/5) and capture voltage, coulomb, temp, (normal full test) as well as SMBus.
306    Ping SMBus a total of 1800 times over the 5 hour test (i.e. every 10 seconds).
307    """
308    old_cycle_function = bms_hardware.csv.cycle
309    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
310    bms_hardware.csv.cycle.create_file()
311    plateset.ce_switch = True  # Allows current charging in excess of 400 mA
312    standard_charge(max_time=6 * 3600, sample_interval=10, charge_current=2)
313    plateset.ce_switch = False
314    standard_discharge(sample_interval=10, discharge_voltage=DISCHARGE_VOLTAGE)
315    bms_hardware.csv.cycle = old_cycle_function
316
317
318def test_low_voltage_charge():
319    """
320    | Requirement          | Charge from any starting voltage  |
321    | :------------------- | :-------------------------------- |
322    | GitHub Issue         | #161 (HITL), #250 (BMS)           |
323    | Instructions         | 1. Power down the cells to 0V to shut down the BMS </br>\
324                             2. Power up the cells to our starting voltage </br>\
325                             3. Start a 2A charge |
326    | Pass / Fail Criteria | Fail if we are unable to charge |
327    | Configuration        | ⦁ `CELL_VOLTAGE` = Starting voltage </br>\
328                             ⦁ `MAX_TIME` = Charge time </br>\
329                             ⦁ `MINIMUM_READINGS` = Minimum low current readings to take before ending early |
330    """
331    old_cycle_function = bms_hardware.csv.cycle
332    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
333    bms_hardware.csv.cycle.create_file()
334
335    logger.write_info_to_report("Powering down cell sims")
336    for cell in bms_hardware.cells.values():
337        cell.disengage_safety_protocols = True
338        cell.volts = 0.0001
339    time.sleep(5)
340    logger.write_info_to_report("Powering up cell sims")
341    for cell in bms_hardware.cells.values():
342        cell.volts = CELL_VOLTAGE
343        cell.disengage_safety_protocols = False
344    time.sleep(5)
345
346    plateset.ce_switch = True  # Allows current charging in excess of 400 mA
347    try:
348        standard_charge(max_time=MAX_TIME, sample_interval=1, charge_current=2, minimum_readings=MINIMUM_READINGS)
349    except TimeoutExceededError:
350        bms_hardware.charger.disable()
351    plateset.ce_switch = False
352
353    bms_hardware.csv.cycle = old_cycle_function
354
355
356def test_0v_works():
357    """
358    | Requirement          | Test charging/discharging with 0V  |
359    | :------------------- | :-------------------------------- |
360    | GitHub Issue         | #161 (HITL), #250 (BMS)           |
361    | Instructions         | 1. Power up BMS </br>\
362                              ⠀⠀⦁ Power cells to a terminal voltage in the range 5.25V-5.75V </br>\
363                              ⠀⠀⦁ Power up to target voltage without causing an imbalance </br>\
364                             3. Discharge for 400 seconds at 2A (down to 0V) </br>\
365                             4. Reset BMS (simulating long discharge at rest) </br>\
366                              ⠀⠀⦁ Power down cell sims to 0V </br>\
367                              ⠀⠀⦁ Power up cell sims to 0.1V </br>\
368                             5. Charge for 200 seconds at 2A </br>\
369                             6. Discharge for 260 seconds at 2A (down to 0V) |
370    | Pass / Fail Criteria | Fail if we are unable to charge or discharge |
371    """
372    old_cycle_function = bms_hardware.csv.cycle
373    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
374    bms_hardware.csv.cycle.create_file()
375    for cell in bms_hardware.cells.values():  # Total voltage = 5.5V
376        cell.exact_volts = 1.375
377    bms_hardware.resting_time = 4
378    bms_hardware.resting_sample_interval = 1
379    bms_hardware.run_resting_cycle()
380
381    for cell in bms_hardware.cells.values():  # Total voltage = 8V
382        cell.exact_volts = 2.0
383    bms_hardware.resting_time = 4
384    bms_hardware.resting_sample_interval = 1
385    bms_hardware.run_resting_cycle()
386
387    for cell in bms_hardware.cells.values():  # Total voltage = 12V (we set it to 2V first to avoid imbalance)
388        cell.exact_volts = 3.0
389
390    try:
391        standard_discharge(max_time=400, discharge_voltage=-0.1, sample_interval=1)
392    except TimeoutExceededError:
393        bms_hardware.load.disable()
394
395    logger.write_info_to_report("Powering down cell sims")
396    for cell in bms_hardware.cells.values():
397        cell.disengage_safety_protocols = True
398        cell.volts = 0.0001
399    time.sleep(5)
400    logger.write_info_to_report("Powering up cell sims")
401    for cell in bms_hardware.cells.values():
402        cell.volts = 0.1
403        cell.disengage_safety_protocols = False
404
405    plateset.ce_switch = True  # Allows current charging in excess of 400 mA
406    try:
407        standard_charge(max_time=200, sample_interval=1, charge_current=2)
408    except TimeoutExceededError:
409        bms_hardware.charger.disable()
410    plateset.ce_switch = False
411
412    try:
413        standard_discharge(max_time=260, discharge_voltage=-0.1, sample_interval=1)
414    except TimeoutExceededError:
415        bms_hardware.load.disable()
416
417    bms_hardware.csv.cycle = old_cycle_function
418
419
420def test_short_discharge():
421    """30 min discharge."""
422    for cell in bms_hardware.cells.values():
423        cell.state_of_charge = 0.10  # 10% FIXME(JA): figure out the correct soc for 20-30 min
424    old_cycle_function = bms_hardware.csv.cycle
425    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
426    bms_hardware.csv.cycle.create_file()
427    standard_discharge(sample_interval=10, discharge_voltage=DISCHARGE_VOLTAGE)
428    bms_hardware.csv.cycle = old_cycle_function
429
430
431def test_timed_discharge():
432    """Timed discharge."""
433    for cell in bms_hardware.cells.values():
434        cell.state_of_charge = 0.80
435    old_cycle_function = bms_hardware.csv.cycle
436    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
437    bms_hardware.csv.cycle.create_file()
438    try:
439        standard_discharge(sample_interval=10, max_time=60 * 60)
440    except TimeoutExceededError:
441        bms_hardware.load.disable()
442    bms_hardware.csv.cycle = old_cycle_function
443
444
445def test_columb_count():
446    """Short discharge, followed by a short rest below -113 mA."""
447    for cell in bms_hardware.cells.values():
448        cell.state_of_charge = 0.80  # 80%
449    old_cycle_function = bms_hardware.csv.cycle
450    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
451    bms_hardware.csv.cycle.create_file()
452    try:
453        standard_discharge(sample_interval=10, max_time=30 * 60)
454    except TimeoutExceededError:
455        pass
456    try:
457        standard_discharge(sample_interval=10, max_time=30 * 60, discharge_current=0.100)
458    except TimeoutExceededError:
459        bms_hardware.load.disable()
460    test_resting(max_time=2 * 60)
461    bms_hardware.csv.cycle = old_cycle_function
462
463
464def test_live_cell_undervoltage():
465    """Discharge down to 2.5 volts and cause an undervoltage discharge fault, clear by CE and charging."""
466    logger.write_info_to_report("Testing Live Cell Undervoltage")
467    serial_watcher.start()
468    cell = None
469    for cell in bms_hardware.cells.values():
470        cell.volts = 3.5
471    if cell:
472        cell.disengage_safety_protocols = True
473        cell.volts = 2.5
474    old_cycle_function = bms_hardware.csv.cycle
475    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
476    bms_hardware.csv.cycle.create_file()
477
478    # Discharge until undervoltage
479    try:
480        standard_discharge(sample_interval=10, discharge_voltage=9)
481    except UnderVoltageError:
482        bms_hardware.load.disable()
483    serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", True)
484
485    # Attempt to charge back up
486    plateset.ce_switch = True  # Allows current charging in excess of 400 mA
487    serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False)
488    try:
489        standard_charge(max_time=2 * 60, sample_interval=10, charge_current=2)
490    except TimeoutExceededError:
491        bms_hardware.charger.disable()
492    plateset.ce_switch = False
493    for cell in bms_hardware.cells.values():
494        assert cell.volts >= 2.6
495        cell.disengage_safety_protocols = False
496
497    bms_hardware.csv.cycle = old_cycle_function
498    serial_watcher.stop()
499
500
501def test_5v_charge():
502    """Check if we can charge the cells back up from as low as 5V."""
503    logger.write_info_to_report("Testing 5V Charging")
504    serial_watcher.start()
505    for cell in bms_hardware.cells.values():
506        cell.disengage_safety_protocols = True
507        cell.volts = 1.4
508    old_cycle_function = bms_hardware.csv.cycle
509    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
510    bms_hardware.csv.cycle.create_file()
511
512    plateset.ce_switch = True  # Allows current charging in excess of 400 mA
513    try:
514        standard_charge(max_time=5 * 60, sample_interval=10, charge_current=2)
515    except TimeoutExceededError:
516        bms_hardware.charger.disable()
517    plateset.ce_switch = False
518
519    for cell in bms_hardware.cells.values():
520        assert cell.volts >= 2.6  # FIXME(JA): set to expected voltage after 5 minutes
521        cell.disengage_safety_protocols = False
522
523    bms_hardware.csv.cycle = old_cycle_function
524    serial_watcher.stop()
525
526
527def test_resting(max_time: int = 30, sample_interval=10):
528    """Record SMBus data while at rest."""
529    old_cycle_function = bms_hardware.csv.cycle
530    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
531    bms_hardware.csv.cycle.create_file()
532    start_time = time.perf_counter()
533    while time.perf_counter() - start_time <= max_time:
534        bms_hardware.csv.cycle.record(time.perf_counter() - start_time, 0.0)
535        time.sleep(sample_interval)
536    bms_hardware.csv.cycle = old_cycle_function
537
538
539def test_standard_rest():
540    """Stabilize the batteries for some time."""
541    old_cycle_function = bms_hardware.csv.cycle
542    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
543    standard_rest()
544    bms_hardware.csv.cycle = old_cycle_function
545
546
547def test_soc_rest():
548    """
549    We can change cell voltages while the state machine is relaxed, does smbus and serial report the proper state of
550    charge?
551    """
552    logger.write_info_to_report("Testing SOC Rest")
553    serial_watcher.start()
554    old_cycle_function = bms_hardware.csv.cycle
555    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
556    bms_hardware.csv.cycle.create_file()
557    for soc in range(10, 100, 10):  # 10% to 90%
558        for cell in bms_hardware.cells.values():
559            cell.state_of_charge = soc / 100
560        test_resting(max_time=30, sample_interval=10)
561        serial_watcher.assert_true("percent_charged", soc, error=(5, 5))
562
563    bms_hardware.csv.cycle = old_cycle_function
564    serial_watcher.stop()
565
566
567def test_soc_discharge():
568    """
569    Discharging: exceed threshold of 113mamps, we discharge at 1 amp for ~30 minutes, have bms report back estimated
570    state of charge, then turn the current off, until it gets back to relaxed. See how the SOC fluctuates during the
571    time between excited and fully relaxed.
572    """
573    logger.write_info_to_report("Testing SOC Discharging")
574    serial_watcher.start()
575    old_cycle_function = bms_hardware.csv.cycle
576    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
577    bms_hardware.csv.cycle.create_file()
578
579    for cell in bms_hardware.cells.values():
580        cell.state_of_charge = 0.80
581    try:
582        standard_discharge(max_time=30 * 60, sample_interval=10, discharge_current=1)
583    except TimeoutExceededError:
584        bms_hardware.load.disable()
585    test_resting(max_time=30 * 60)
586
587    bms_hardware.csv.cycle = old_cycle_function
588    serial_watcher.stop()
589
590
591def test_soc_charge():
592    """
593    Charging: exceed threshold of 143mamps, we charge at 1 amp for ~30 minutes, have bms report back estimated
594    state of charge, then turn the current off, until it gets back to relaxed. See how the SOC fluctuates during the
595    time between excited and fully relaxed.
596    """
597    logger.write_info_to_report("Testing SOC Charging")
598    serial_watcher.start()
599    old_cycle_function = bms_hardware.csv.cycle
600    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
601    bms_hardware.csv.cycle.create_file()
602
603    for cell in bms_hardware.cells.values():
604        cell.state_of_charge = 0.20
605    plateset.ce_switch = True
606    try:
607        standard_charge(max_time=30 * 60, sample_interval=10, charge_current=1)
608    except TimeoutExceededError:
609        bms_hardware.charger.disable()
610    plateset.ce_switch = False
611    test_resting(max_time=30 * 60)
612
613    bms_hardware.csv.cycle = old_cycle_function
614    serial_watcher.stop()
615
616
617def test_wakeup_counter():
618    """
619    Charge at around 10mA to repeatedly trigger the wakeup counter.
620    This occurs as we are very close to the trigger point.
621    """
622    logger.write_info_to_report("Testing Wakeup Counter")
623    old_cycle_function = bms_hardware.csv.cycle
624    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
625    bms_hardware.csv.cycle.create_file()
626    serial_watcher.start(csv=True)
627
628    for cell in bms_hardware.cells.values():
629        cell.state_of_charge = 0.20
630    plateset.ce_switch = True
631    try:
632        standard_charge(max_time=5 * 60, sample_interval=1, charge_current=WAKEUP_CURRENT, termination_current=0.000001)
633    except TimeoutExceededError:
634        bms_hardware.charger.disable()
635    plateset.ce_switch = False
636
637    assert serial_watcher.events["Wakeup_Counter"][0].value == serial_watcher.events["Wakeup_Counter"][-1].value
638
639    serial_watcher.stop()
640    bms_hardware.csv.cycle = old_cycle_function
641
642
643def test_charge_increment():
644    """Increment current."""
645    logger.write_info_to_report("Charging Incrementally")
646    old_cycle_function = bms_hardware.csv.cycle
647    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
648
649    max_time = 3 * 60 * 60
650    elapsed_time = 0
651    charge_sample_interval = 1
652    termination_current = 0.0
653    current_limit = CHARGE_INC_STOP
654    charge_current = CHARGE_INC_START
655    latest_i = termination_current + 0.1
656    ov_protection = 17
657    latest_v = 0
658    max_charge_current = 0
659
660    current_time = StopWatch()
661    current_time.start()
662
663    # Enable charging and timer
664    plateset.ce_switch = True  # Does not allow current charging in excess of 400 mA or less
665    bms_hardware.charger.set_profile(volts=16.8, amps=charge_current)
666    bms_hardware.charger.enable()  # Enables the output of the charger
667    bms_hardware.timer.reset()  # Keep track of runtime
668
669    # Charge until termination current, current limit, ov_protection, or max_time
670    while current_limit >= latest_i >= termination_current:
671        if latest_v >= ov_protection:
672            raise OverVoltageError(
673                f"Overvoltage protection triggered at {time.strftime('%x %X')}. "
674                f"Voltage {latest_v} is higher than {ov_protection}."
675            )
676        if elapsed_time >= max_time:
677            raise TimeoutExceededError(f"Current of {current_limit}A was not reached after {max_time} seconds")
678
679        elapsed_time = bms_hardware.timer.elapsed_time
680        latest_v = bms_hardware.dmm.volts
681        latest_i = bms_hardware.charger.amps
682        max_charge_current = max(max_charge_current, latest_i)
683        logger.write_info_to_report(f"Elapsed Time: {elapsed_time}, Voltage: {latest_v}, Current: {latest_i}")
684
685        bms_hardware.csv.cycle.record(elapsed_time)
686        if current_time.elapsed_time > CHARGE_INC_TIME:
687            current_time.reset()
688            charge_current += CHARGE_INC_STEP
689        bms_hardware.charger.set_profile(volts=16.8, amps=charge_current)
690        time.sleep(charge_sample_interval)
691    bms_hardware.charger.disable()  # Charging is complete, turn off the charger
692    plateset.ce_switch = False
693    bms_hardware.csv.cycle = old_cycle_function
694
695
696def test_cell_sim_voltage():
697    """Set the cell sims to some voltage."""
698    # old_cycle_function = bms_hardware.csv.cycle
699    # bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
700    # bms_hardware.csv.cycle.create_file()
701
702    logger.write_info_to_report("Powering down cell sims")
703    for cell in bms_hardware.cells.values():
704        cell.disengage_safety_protocols = True
705        cell.volts = 0.0001
706    time.sleep(5)
707    logger.write_info_to_report("Powering up cell sims")
708    for cell in bms_hardware.cells.values():
709        cell.volts = CELL_VOLTAGE
710        cell.disengage_safety_protocols = False
711    # for cell in bms_hardware.cells.values():
712    #    cell.exact_volts = CELL_VOLTAGE
713    logger.write_info_to_report("Setting temperature to 15°C")
714    plateset.thermistor1 = DEFAULT_TEMPERATURE_C
715    plateset.thermistor2 = DEFAULT_TEMPERATURE_C
716
717    time.sleep(5)
718
719    standard_discharge(sample_interval=10, discharge_current=DISCHARGE_CURRENT / 1000, discharge_voltage=-0.05)
720
721    bms_hardware.resting_time = REST_TIME
722    bms_hardware.max_time = REST_TIME
723    bms_hardware.resting_sample_interval = REST_INTERVAL
724    bms_hardware.sample_interval = REST_INTERVAL
725    bms_hardware.run_resting_cycle()
726
727    # bms_hardware.csv.cycle = old_cycle_function
728
729
730def test_cycle_count():
731    """Repeat capacity test to confirm cycle count functionality."""
732    test_reset_cell_sims()
733
734    old_cycle_function = bms_hardware.csv.cycle
735    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
736    bms_hardware.csv.cycle.create_file()
737    # serial_watcher.start(csv=True)
738
739    # Change cell sim capacity
740    for cell in bms_hardware.cells.values():
741        cell.data.capacity = CYCLE_CAPACITY
742
743    # Run cycles
744    for i in range(CYCLE_COUNT):
745        logger.write_info_to_report(f"Running cycle {i}")
746        plateset.ce_switch = True  # Allows current charging in excess of 400 mA
747        standard_charge(termination_current=1.0)
748        plateset.ce_switch = False
749        standard_discharge(discharge_voltage=2.9 * 4)
750
751    # serial_watcher.stop()
752    bms_hardware.csv.cycle = old_cycle_function
753
754    # FIXME(JA): merge NGI branch to allow threads to access the same hardware
755    # assert serial_watcher.events["charge_cycles"][-1].value == CYCLE_COUNT
MAX_TIME = 30
CELL_VOLTAGE = 1.375
DEFAULT_SOC_PERCENT = 0.8
DEFAULT_TEMPERATURE_C = 15
REST_TIME = 10800
REST_INTERVAL = 10
DISCHARGE_VOLTAGE = 10
DISCHARGE_CURRENT = 2
MINIMUM_READINGS = 3
CHARGE_INC_START = 0
CHARGE_INC_STOP = 3
CHARGE_INC_STEP = 0.02
CHARGE_INC_TIME = 20
SAMPLE_INTERVAL = 10
WAKEUP_CURRENT = 0.01
CYCLE_COUNT = 6
CYCLE_CAPACITY = 0.6
def test_reset_cell_sims():
55def test_reset_cell_sims():
56    """Activate cell sims and set appropriate temperatures."""
57    logger.write_info_to_report("Powering down cell sims")
58    for cell in bms_hardware.cells.values():
59        cell.disengage_safety_protocols = True
60        cell.volts = 0.0001
61    time.sleep(5)
62    logger.write_info_to_report("Powering up cell sims")
63    for cell in bms_hardware.cells.values():
64        cell.state_of_charge = DEFAULT_SOC_PERCENT
65        cell.disengage_safety_protocols = False
66    logger.write_info_to_report(f"Setting temperature to {DEFAULT_TEMPERATURE_C}°C")
67    plateset.thermistor1 = DEFAULT_TEMPERATURE_C
68    plateset.thermistor2 = DEFAULT_TEMPERATURE_C

Activate cell sims and set appropriate temperatures.

def check_cell_imbalance_conditions(high_cell_id: int):
 71def check_cell_imbalance_conditions(high_cell_id: int):
 72    """Set one cell 100mV above the rest, confirm its flag is set, and that it has a higher current."""
 73    serial_watcher.assert_true(f"flags.output_c{high_cell_id}_bal", False)
 74    old_cell_current = bms_hardware.cells[high_cell_id].amps  # NOTE: Do I need to be (dis)charging?
 75    for i, cell in bms_hardware.cells.items():
 76        logger.write_debug_to_report(f"Cell {i}: {cell.measured_volts} V ({cell.volts}), {cell.amps} A")
 77
 78    logger.write_info_to_report(f"Cell {high_cell_id} = 4.0 V")
 79    bms_hardware.cells[high_cell_id].exact_volts = 4.0
 80    for cell_id in bms_hardware.cells:
 81        serial_watcher.assert_true(f"flags.output_c{cell_id}_bal", cell_id == high_cell_id)
 82
 83    # We should see the current increase when a flag is set. The current will be cell voltage / 100
 84    cell_current = bms_hardware.cells[high_cell_id].amps
 85    for i, cell in bms_hardware.cells.items():
 86        logger.write_debug_to_report(f"Cell {i}: {cell.measured_volts} V ({cell.volts}), {cell.amps} A")
 87    logger.write_info_to_report(f"{cell_current} A > {old_cell_current} A?")
 88    assert cell_current > old_cell_current
 89    new_cell_current = cell_current - old_cell_current
 90    expected_current_a = bms_hardware.cells[high_cell_id].volts / 100
 91    logger.write_info_to_report(f"{new_cell_current} A ≈ {expected_current_a} A")
 92    current_error_a = 0.02
 93    assert expected_current_a - current_error_a <= new_cell_current <= expected_current_a + current_error_a
 94
 95    # Reset the higher voltage
 96    logger.write_info_to_report(f"Cell {high_cell_id} = 3.9 V")
 97    bms_hardware.cells[high_cell_id].exact_volts = 3.9
 98    for i, cell in bms_hardware.cells.items():
 99        logger.write_debug_to_report(f"Cell {i}: {cell.measured_volts} V (target: {cell.volts}), {cell.amps} A")
100    for cell_id in bms_hardware.cells:
101        serial_watcher.assert_true(f"flags.output_c{cell_id}_bal", False)

Set one cell 100mV above the rest, confirm its flag is set, and that it has a higher current.

def test_cell_imbalance():
104def test_cell_imbalance():
105    """
106    The flag is set when a cell is 4.2V+ or 20mv above the lowest cell.
107
108    What that is actually doing though is connecting a resistor across the cell that is too high in voltage.
109    If you monitor the cell simulator current you should actually see the current increase in that cell if
110    the flag get set. The current would be Vcell / 100 since they use 100 Ohm resistors.
111    """
112    logger.write_info_to_report("Testing Cell Imbalance")
113    serial_watcher.start()
114    for cell in bms_hardware.cells.values():
115        cell.exact_volts = 3.9
116    for cell_id in bms_hardware.cells:
117        serial_watcher.assert_true(f"flags.output_c{cell_id}_bal", False)
118
119    # Resting
120    logger.write_info_to_report("Resting")
121    for cell_id in bms_hardware.cells:
122        check_cell_imbalance_conditions(cell_id)
123
124    # Discharging
125    logger.write_info_to_report("Discharging at 100 mA")
126    bms_hardware.load.mode_cc()
127    bms_hardware.load.amps = 0.100
128    bms_hardware.load.enable()
129    for cell_id in bms_hardware.cells:
130        check_cell_imbalance_conditions(cell_id)
131    bms_hardware.load.disable()
132
133    # Charging
134    logger.write_info_to_report("Charging at 100 mA")
135    bms_hardware.charger.set_profile(3.9 * 4, 0.1)
136    plateset.ce_switch = True
137    bms_hardware.charger.enable()
138    for cell_id in bms_hardware.cells:
139        check_cell_imbalance_conditions(cell_id)
140    bms_hardware.charger.disable()
141    plateset.ce_switch = False
142
143    serial_watcher.stop()

The flag is set when a cell is 4.2V+ or 20mv above the lowest cell.

What that is actually doing though is connecting a resistor across the cell that is too high in voltage. If you monitor the cell simulator current you should actually see the current increase in that cell if the flag get set. The current would be Vcell / 100 since they use 100 Ohm resistors.

def test_charge_enable():
146def test_charge_enable():
147    """
148    Confirm we can charge in excess of 400 mA when charge enable is on.
149    If battery is near full charge, you may not be able to go past 400 mA when charging.
150
151    We want to disable the charging current if charge enable is off if the current goes over 400 mA per
152    the spec. In reality, we want to disable if current is over 20 mA since that's what BT does and it's a safer way
153    to charge. Other OTS may have different cutoffs, and the BT one was measured by experiment.
154    """
155    logger.write_info_to_report("Testing Charge Enable")
156    serial_watcher.start()
157
158    logger.write_info_to_report("Charge enable = 1")
159    plateset.ce_switch = True  # Allows current charging in excess of 400 mA
160    serial_watcher.assert_true("ncharge_en_gpio", False, 1)
161    max_time = 60
162    elapsed_time = 0
163    charge_sample_interval = 5
164    termination_current = 0.100
165    current_limit = 0.400 + 0.100  # When to end the test
166    latest_i = termination_current + 0.1
167    ov_protection = 17
168    latest_v = 0
169
170    # Enable charging and timer
171    bms_hardware.charger.set_profile(volts=16.8, amps=3)
172    bms_hardware.charger.enable()  # Enables the output of the charger
173    bms_hardware.timer.reset()  # Keep track of runtime
174
175    # Charge until termination current, current limit, ov_protection, or max_time
176    while current_limit >= latest_i >= termination_current:
177        if latest_v >= ov_protection:
178            raise OverVoltageError(
179                f"Overvoltage protection triggered at {time.strftime('%x %X')}. "
180                f"Voltage {latest_v} is higher than {ov_protection}."
181            )
182        if elapsed_time >= max_time:
183            raise TimeoutExceededError(f"Current of {current_limit}A was not reached after {max_time} seconds")
184
185        elapsed_time = bms_hardware.timer.elapsed_time
186        latest_v = bms_hardware.dmm.volts
187        latest_i = bms_hardware.charger.amps
188        logger.write_info_to_report(f"Elapsed Time: {elapsed_time}, Voltage: {latest_v}, Current: {latest_i}")
189
190        bms_hardware.csv.cycle.record(elapsed_time)
191        time.sleep(charge_sample_interval)
192    bms_hardware.charger.disable()  # Charging is complete, turn off the charger
193
194    assert latest_i > current_limit
195
196    logger.write_info_to_report("Charge enable = 0")
197    plateset.ce_switch = False  # Does not allow current charging in excess of 400 mA or less
198    serial_watcher.assert_true("ncharge_en_gpio", True, 2)
199    max_time = 60
200    elapsed_time = 0
201    charge_sample_interval = 1
202    termination_current = 0.0
203    current_limit = 0.400 + 0.100  # When to end the test
204    charge_current = 0.001
205    latest_i = termination_current + 0.1
206    ov_protection = 17
207    latest_v = 0
208    max_charge_current = 0
209
210    # Enable charging and timer
211    bms_hardware.charger.set_profile(volts=16.8, amps=charge_current)
212    bms_hardware.charger.enable()  # Enables the output of the charger
213    bms_hardware.timer.reset()  # Keep track of runtime
214
215    # Charge until termination current, current limit, ov_protection, or max_time
216    while current_limit >= latest_i >= termination_current:
217        if latest_v >= ov_protection:
218            raise OverVoltageError(
219                f"Overvoltage protection triggered at {time.strftime('%x %X')}. "
220                f"Voltage {latest_v} is higher than {ov_protection}."
221            )
222        if elapsed_time >= max_time:
223            raise TimeoutExceededError(f"Current of {current_limit}A was not reached after {max_time} seconds")
224
225        elapsed_time = bms_hardware.timer.elapsed_time
226        latest_v = bms_hardware.dmm.volts
227        latest_i = bms_hardware.charger.amps
228        max_charge_current = max(max_charge_current, latest_i)
229        logger.write_info_to_report(f"Elapsed Time: {elapsed_time}, Voltage: {latest_v}, Current: {latest_i}")
230
231        bms_hardware.csv.cycle.record(elapsed_time)
232        charge_current += 0.001  # Increase by 1mA per second
233        bms_hardware.charger.set_profile(volts=16.8, amps=charge_current)
234        time.sleep(charge_sample_interval)
235    bms_hardware.charger.disable()  # Charging is complete, turn off the charger
236
237    assert max_charge_current <= 0.400
238
239    serial_watcher.stop()
240    test_reset_cell_sims()

Confirm we can charge in excess of 400 mA when charge enable is on. If battery is near full charge, you may not be able to go past 400 mA when charging.

We want to disable the charging current if charge enable is off if the current goes over 400 mA per the spec. In reality, 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.

def standard_charge( charge_current: float = 2, max_time: int = 10800, sample_interval: int = 10, minimum_readings: int = 3, termination_current: float = 0.1):
243def standard_charge(
244    charge_current: float = 2,
245    max_time: int = 3 * 3600,
246    sample_interval: int = 10,
247    minimum_readings: int = 3,
248    termination_current: float = 0.100,
249):
250    """
251    Charge batteries in accordance with 4.3.1 for not greater than three hours.
252    4.3.1 = 23 ± 5°C (73.4°F) ambient pressure/relative humidity, with 2+ hours between charge and discharge.
253    """
254    bms_hardware.voltage = 16.8
255    bms_hardware.ov_protection = bms_hardware.voltage + 0.050  # 50mV above the charging voltage
256    bms_hardware.current = charge_current
257    bms_hardware.termination_current = termination_current  # 100 mA
258    bms_hardware.max_time = max_time
259    bms_hardware.sample_interval = sample_interval
260    bms_hardware.minimum_readings = minimum_readings
261
262    # Run the Charge cycle
263    bms_hardware.run_li_charge_cycle()

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_discharge( discharge_current: float = 2, max_time: int = 28800, sample_interval: int = 10, discharge_voltage: float = 10):
266def standard_discharge(
267    discharge_current: float = DISCHARGE_CURRENT,
268    max_time: int = 8 * 3600,
269    sample_interval: int = 10,
270    discharge_voltage: float = DISCHARGE_VOLTAGE,
271):
272    """Discharge at 2A until 10V."""
273    bms_hardware.voltage = discharge_voltage
274    bms_hardware.uv_protection = bms_hardware.voltage - 0.500  # 500mV below voltage cutoff
275    bms_hardware.current = discharge_current
276    bms_hardware.discharge_type = DischargeType.CONSTANT_CURRENT
277    bms_hardware.max_time = max_time
278    bms_hardware.sample_interval = sample_interval
279
280    # Run the discharge cycle, returning the capacity
281    capacity = bms_hardware.run_discharge_cycle()
282    logger.write_info_to_report(f"Discharge complete, capacity was {capacity * 1000.0} mAh")
283    return capacity

Discharge at 2A until 10V.

def standard_rest(seconds: int = 0, sample_interval: int = 0):
286def standard_rest(seconds: int = 0, sample_interval: int = 0):
287    """Stabilize the batteries for 2+ hours."""
288    bms_hardware.max_time = seconds or REST_TIME
289    bms_hardware.sample_interval = sample_interval or SAMPLE_INTERVAL
290    bms_hardware.run_resting_cycle()

Stabilize the batteries for 2+ hours.

def test_full_discharge():
293def test_full_discharge():
294    """Perform a discharge from full."""
295    for cell in bms_hardware.cells.values():
296        cell.state_of_charge = 1.00
297    old_cycle_function = bms_hardware.csv.cycle
298    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
299    bms_hardware.csv.cycle.create_file()
300    standard_discharge(sample_interval=10)
301    bms_hardware.csv.cycle = old_cycle_function

Perform a discharge from full.

def test_capacity_discharge():
304def test_capacity_discharge():
305    """
306    Discharge at 2A (per Mil-prf/5) and capture voltage, coulomb, temp, (normal full test) as well as SMBus.
307    Ping SMBus a total of 1800 times over the 5 hour test (i.e. every 10 seconds).
308    """
309    old_cycle_function = bms_hardware.csv.cycle
310    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
311    bms_hardware.csv.cycle.create_file()
312    plateset.ce_switch = True  # Allows current charging in excess of 400 mA
313    standard_charge(max_time=6 * 3600, sample_interval=10, charge_current=2)
314    plateset.ce_switch = False
315    standard_discharge(sample_interval=10, discharge_voltage=DISCHARGE_VOLTAGE)
316    bms_hardware.csv.cycle = old_cycle_function

Discharge at 2A (per Mil-prf/5) and capture voltage, coulomb, temp, (normal full test) as well as SMBus. Ping SMBus a total of 1800 times over the 5 hour test (i.e. every 10 seconds).

def test_low_voltage_charge():
319def test_low_voltage_charge():
320    """
321    | Requirement          | Charge from any starting voltage  |
322    | :------------------- | :-------------------------------- |
323    | GitHub Issue         | #161 (HITL), #250 (BMS)           |
324    | Instructions         | 1. Power down the cells to 0V to shut down the BMS </br>\
325                             2. Power up the cells to our starting voltage </br>\
326                             3. Start a 2A charge |
327    | Pass / Fail Criteria | Fail if we are unable to charge |
328    | Configuration        | ⦁ `CELL_VOLTAGE` = Starting voltage </br>\
329                             ⦁ `MAX_TIME` = Charge time </br>\
330                             ⦁ `MINIMUM_READINGS` = Minimum low current readings to take before ending early |
331    """
332    old_cycle_function = bms_hardware.csv.cycle
333    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
334    bms_hardware.csv.cycle.create_file()
335
336    logger.write_info_to_report("Powering down cell sims")
337    for cell in bms_hardware.cells.values():
338        cell.disengage_safety_protocols = True
339        cell.volts = 0.0001
340    time.sleep(5)
341    logger.write_info_to_report("Powering up cell sims")
342    for cell in bms_hardware.cells.values():
343        cell.volts = CELL_VOLTAGE
344        cell.disengage_safety_protocols = False
345    time.sleep(5)
346
347    plateset.ce_switch = True  # Allows current charging in excess of 400 mA
348    try:
349        standard_charge(max_time=MAX_TIME, sample_interval=1, charge_current=2, minimum_readings=MINIMUM_READINGS)
350    except TimeoutExceededError:
351        bms_hardware.charger.disable()
352    plateset.ce_switch = False
353
354    bms_hardware.csv.cycle = old_cycle_function
Requirement Charge from any starting voltage
GitHub Issue #161 (HITL), #250 (BMS)
Instructions 1. Power down the cells to 0V to shut down the BMS
2. Power up the cells to our starting voltage
3. Start a 2A charge
Pass / Fail Criteria Fail if we are unable to charge
Configuration CELL_VOLTAGE = Starting voltage
MAX_TIME = Charge time
MINIMUM_READINGS = Minimum low current readings to take before ending early
def test_0v_works():
357def test_0v_works():
358    """
359    | Requirement          | Test charging/discharging with 0V  |
360    | :------------------- | :-------------------------------- |
361    | GitHub Issue         | #161 (HITL), #250 (BMS)           |
362    | Instructions         | 1. Power up BMS </br>\
363                              ⠀⠀⦁ Power cells to a terminal voltage in the range 5.25V-5.75V </br>\
364                              ⠀⠀⦁ Power up to target voltage without causing an imbalance </br>\
365                             3. Discharge for 400 seconds at 2A (down to 0V) </br>\
366                             4. Reset BMS (simulating long discharge at rest) </br>\
367                              ⠀⠀⦁ Power down cell sims to 0V </br>\
368                              ⠀⠀⦁ Power up cell sims to 0.1V </br>\
369                             5. Charge for 200 seconds at 2A </br>\
370                             6. Discharge for 260 seconds at 2A (down to 0V) |
371    | Pass / Fail Criteria | Fail if we are unable to charge or discharge |
372    """
373    old_cycle_function = bms_hardware.csv.cycle
374    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
375    bms_hardware.csv.cycle.create_file()
376    for cell in bms_hardware.cells.values():  # Total voltage = 5.5V
377        cell.exact_volts = 1.375
378    bms_hardware.resting_time = 4
379    bms_hardware.resting_sample_interval = 1
380    bms_hardware.run_resting_cycle()
381
382    for cell in bms_hardware.cells.values():  # Total voltage = 8V
383        cell.exact_volts = 2.0
384    bms_hardware.resting_time = 4
385    bms_hardware.resting_sample_interval = 1
386    bms_hardware.run_resting_cycle()
387
388    for cell in bms_hardware.cells.values():  # Total voltage = 12V (we set it to 2V first to avoid imbalance)
389        cell.exact_volts = 3.0
390
391    try:
392        standard_discharge(max_time=400, discharge_voltage=-0.1, sample_interval=1)
393    except TimeoutExceededError:
394        bms_hardware.load.disable()
395
396    logger.write_info_to_report("Powering down cell sims")
397    for cell in bms_hardware.cells.values():
398        cell.disengage_safety_protocols = True
399        cell.volts = 0.0001
400    time.sleep(5)
401    logger.write_info_to_report("Powering up cell sims")
402    for cell in bms_hardware.cells.values():
403        cell.volts = 0.1
404        cell.disengage_safety_protocols = False
405
406    plateset.ce_switch = True  # Allows current charging in excess of 400 mA
407    try:
408        standard_charge(max_time=200, sample_interval=1, charge_current=2)
409    except TimeoutExceededError:
410        bms_hardware.charger.disable()
411    plateset.ce_switch = False
412
413    try:
414        standard_discharge(max_time=260, discharge_voltage=-0.1, sample_interval=1)
415    except TimeoutExceededError:
416        bms_hardware.load.disable()
417
418    bms_hardware.csv.cycle = old_cycle_function
Requirement Test charging/discharging with 0V
GitHub Issue #161 (HITL), #250 (BMS)
Instructions 1. Power up BMS
⠀⠀⦁ Power cells to a terminal voltage in the range 5.25V-5.75V
⠀⠀⦁ Power up to target voltage without causing an imbalance
3. Discharge for 400 seconds at 2A (down to 0V)
4. Reset BMS (simulating long discharge at rest)
⠀⠀⦁ Power down cell sims to 0V
⠀⠀⦁ Power up cell sims to 0.1V
5. Charge for 200 seconds at 2A
6. Discharge for 260 seconds at 2A (down to 0V)
Pass / Fail Criteria Fail if we are unable to charge or discharge
def test_short_discharge():
421def test_short_discharge():
422    """30 min discharge."""
423    for cell in bms_hardware.cells.values():
424        cell.state_of_charge = 0.10  # 10% FIXME(JA): figure out the correct soc for 20-30 min
425    old_cycle_function = bms_hardware.csv.cycle
426    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
427    bms_hardware.csv.cycle.create_file()
428    standard_discharge(sample_interval=10, discharge_voltage=DISCHARGE_VOLTAGE)
429    bms_hardware.csv.cycle = old_cycle_function

30 min discharge.

def test_timed_discharge():
432def test_timed_discharge():
433    """Timed discharge."""
434    for cell in bms_hardware.cells.values():
435        cell.state_of_charge = 0.80
436    old_cycle_function = bms_hardware.csv.cycle
437    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
438    bms_hardware.csv.cycle.create_file()
439    try:
440        standard_discharge(sample_interval=10, max_time=60 * 60)
441    except TimeoutExceededError:
442        bms_hardware.load.disable()
443    bms_hardware.csv.cycle = old_cycle_function

Timed discharge.

def test_columb_count():
446def test_columb_count():
447    """Short discharge, followed by a short rest below -113 mA."""
448    for cell in bms_hardware.cells.values():
449        cell.state_of_charge = 0.80  # 80%
450    old_cycle_function = bms_hardware.csv.cycle
451    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
452    bms_hardware.csv.cycle.create_file()
453    try:
454        standard_discharge(sample_interval=10, max_time=30 * 60)
455    except TimeoutExceededError:
456        pass
457    try:
458        standard_discharge(sample_interval=10, max_time=30 * 60, discharge_current=0.100)
459    except TimeoutExceededError:
460        bms_hardware.load.disable()
461    test_resting(max_time=2 * 60)
462    bms_hardware.csv.cycle = old_cycle_function

Short discharge, followed by a short rest below -113 mA.

def test_live_cell_undervoltage():
465def test_live_cell_undervoltage():
466    """Discharge down to 2.5 volts and cause an undervoltage discharge fault, clear by CE and charging."""
467    logger.write_info_to_report("Testing Live Cell Undervoltage")
468    serial_watcher.start()
469    cell = None
470    for cell in bms_hardware.cells.values():
471        cell.volts = 3.5
472    if cell:
473        cell.disengage_safety_protocols = True
474        cell.volts = 2.5
475    old_cycle_function = bms_hardware.csv.cycle
476    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
477    bms_hardware.csv.cycle.create_file()
478
479    # Discharge until undervoltage
480    try:
481        standard_discharge(sample_interval=10, discharge_voltage=9)
482    except UnderVoltageError:
483        bms_hardware.load.disable()
484    serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", True)
485
486    # Attempt to charge back up
487    plateset.ce_switch = True  # Allows current charging in excess of 400 mA
488    serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False)
489    try:
490        standard_charge(max_time=2 * 60, sample_interval=10, charge_current=2)
491    except TimeoutExceededError:
492        bms_hardware.charger.disable()
493    plateset.ce_switch = False
494    for cell in bms_hardware.cells.values():
495        assert cell.volts >= 2.6
496        cell.disengage_safety_protocols = False
497
498    bms_hardware.csv.cycle = old_cycle_function
499    serial_watcher.stop()

Discharge down to 2.5 volts and cause an undervoltage discharge fault, clear by CE and charging.

def test_5v_charge():
502def test_5v_charge():
503    """Check if we can charge the cells back up from as low as 5V."""
504    logger.write_info_to_report("Testing 5V Charging")
505    serial_watcher.start()
506    for cell in bms_hardware.cells.values():
507        cell.disengage_safety_protocols = True
508        cell.volts = 1.4
509    old_cycle_function = bms_hardware.csv.cycle
510    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
511    bms_hardware.csv.cycle.create_file()
512
513    plateset.ce_switch = True  # Allows current charging in excess of 400 mA
514    try:
515        standard_charge(max_time=5 * 60, sample_interval=10, charge_current=2)
516    except TimeoutExceededError:
517        bms_hardware.charger.disable()
518    plateset.ce_switch = False
519
520    for cell in bms_hardware.cells.values():
521        assert cell.volts >= 2.6  # FIXME(JA): set to expected voltage after 5 minutes
522        cell.disengage_safety_protocols = False
523
524    bms_hardware.csv.cycle = old_cycle_function
525    serial_watcher.stop()

Check if we can charge the cells back up from as low as 5V.

def test_resting(max_time: int = 30, sample_interval=10):
528def test_resting(max_time: int = 30, sample_interval=10):
529    """Record SMBus data while at rest."""
530    old_cycle_function = bms_hardware.csv.cycle
531    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
532    bms_hardware.csv.cycle.create_file()
533    start_time = time.perf_counter()
534    while time.perf_counter() - start_time <= max_time:
535        bms_hardware.csv.cycle.record(time.perf_counter() - start_time, 0.0)
536        time.sleep(sample_interval)
537    bms_hardware.csv.cycle = old_cycle_function

Record SMBus data while at rest.

def test_standard_rest():
540def test_standard_rest():
541    """Stabilize the batteries for some time."""
542    old_cycle_function = bms_hardware.csv.cycle
543    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
544    standard_rest()
545    bms_hardware.csv.cycle = old_cycle_function

Stabilize the batteries for some time.

def test_soc_rest():
548def test_soc_rest():
549    """
550    We can change cell voltages while the state machine is relaxed, does smbus and serial report the proper state of
551    charge?
552    """
553    logger.write_info_to_report("Testing SOC Rest")
554    serial_watcher.start()
555    old_cycle_function = bms_hardware.csv.cycle
556    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
557    bms_hardware.csv.cycle.create_file()
558    for soc in range(10, 100, 10):  # 10% to 90%
559        for cell in bms_hardware.cells.values():
560            cell.state_of_charge = soc / 100
561        test_resting(max_time=30, sample_interval=10)
562        serial_watcher.assert_true("percent_charged", soc, error=(5, 5))
563
564    bms_hardware.csv.cycle = old_cycle_function
565    serial_watcher.stop()

We can change cell voltages while the state machine is relaxed, does smbus and serial report the proper state of charge?

def test_soc_discharge():
568def test_soc_discharge():
569    """
570    Discharging: exceed threshold of 113mamps, we discharge at 1 amp for ~30 minutes, have bms report back estimated
571    state of charge, then turn the current off, until it gets back to relaxed. See how the SOC fluctuates during the
572    time between excited and fully relaxed.
573    """
574    logger.write_info_to_report("Testing SOC Discharging")
575    serial_watcher.start()
576    old_cycle_function = bms_hardware.csv.cycle
577    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
578    bms_hardware.csv.cycle.create_file()
579
580    for cell in bms_hardware.cells.values():
581        cell.state_of_charge = 0.80
582    try:
583        standard_discharge(max_time=30 * 60, sample_interval=10, discharge_current=1)
584    except TimeoutExceededError:
585        bms_hardware.load.disable()
586    test_resting(max_time=30 * 60)
587
588    bms_hardware.csv.cycle = old_cycle_function
589    serial_watcher.stop()

Discharging: exceed threshold of 113mamps, we discharge at 1 amp for ~30 minutes, have bms report back estimated state of charge, then turn the current off, until it gets back to relaxed. See how the SOC fluctuates during the time between excited and fully relaxed.

def test_soc_charge():
592def test_soc_charge():
593    """
594    Charging: exceed threshold of 143mamps, we charge at 1 amp for ~30 minutes, have bms report back estimated
595    state of charge, then turn the current off, until it gets back to relaxed. See how the SOC fluctuates during the
596    time between excited and fully relaxed.
597    """
598    logger.write_info_to_report("Testing SOC Charging")
599    serial_watcher.start()
600    old_cycle_function = bms_hardware.csv.cycle
601    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
602    bms_hardware.csv.cycle.create_file()
603
604    for cell in bms_hardware.cells.values():
605        cell.state_of_charge = 0.20
606    plateset.ce_switch = True
607    try:
608        standard_charge(max_time=30 * 60, sample_interval=10, charge_current=1)
609    except TimeoutExceededError:
610        bms_hardware.charger.disable()
611    plateset.ce_switch = False
612    test_resting(max_time=30 * 60)
613
614    bms_hardware.csv.cycle = old_cycle_function
615    serial_watcher.stop()

Charging: exceed threshold of 143mamps, we charge at 1 amp for ~30 minutes, have bms report back estimated state of charge, then turn the current off, until it gets back to relaxed. See how the SOC fluctuates during the time between excited and fully relaxed.

def test_wakeup_counter():
618def test_wakeup_counter():
619    """
620    Charge at around 10mA to repeatedly trigger the wakeup counter.
621    This occurs as we are very close to the trigger point.
622    """
623    logger.write_info_to_report("Testing Wakeup Counter")
624    old_cycle_function = bms_hardware.csv.cycle
625    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
626    bms_hardware.csv.cycle.create_file()
627    serial_watcher.start(csv=True)
628
629    for cell in bms_hardware.cells.values():
630        cell.state_of_charge = 0.20
631    plateset.ce_switch = True
632    try:
633        standard_charge(max_time=5 * 60, sample_interval=1, charge_current=WAKEUP_CURRENT, termination_current=0.000001)
634    except TimeoutExceededError:
635        bms_hardware.charger.disable()
636    plateset.ce_switch = False
637
638    assert serial_watcher.events["Wakeup_Counter"][0].value == serial_watcher.events["Wakeup_Counter"][-1].value
639
640    serial_watcher.stop()
641    bms_hardware.csv.cycle = old_cycle_function

Charge at around 10mA to repeatedly trigger the wakeup counter. This occurs as we are very close to the trigger point.

def test_charge_increment():
644def test_charge_increment():
645    """Increment current."""
646    logger.write_info_to_report("Charging Incrementally")
647    old_cycle_function = bms_hardware.csv.cycle
648    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
649
650    max_time = 3 * 60 * 60
651    elapsed_time = 0
652    charge_sample_interval = 1
653    termination_current = 0.0
654    current_limit = CHARGE_INC_STOP
655    charge_current = CHARGE_INC_START
656    latest_i = termination_current + 0.1
657    ov_protection = 17
658    latest_v = 0
659    max_charge_current = 0
660
661    current_time = StopWatch()
662    current_time.start()
663
664    # Enable charging and timer
665    plateset.ce_switch = True  # Does not allow current charging in excess of 400 mA or less
666    bms_hardware.charger.set_profile(volts=16.8, amps=charge_current)
667    bms_hardware.charger.enable()  # Enables the output of the charger
668    bms_hardware.timer.reset()  # Keep track of runtime
669
670    # Charge until termination current, current limit, ov_protection, or max_time
671    while current_limit >= latest_i >= termination_current:
672        if latest_v >= ov_protection:
673            raise OverVoltageError(
674                f"Overvoltage protection triggered at {time.strftime('%x %X')}. "
675                f"Voltage {latest_v} is higher than {ov_protection}."
676            )
677        if elapsed_time >= max_time:
678            raise TimeoutExceededError(f"Current of {current_limit}A was not reached after {max_time} seconds")
679
680        elapsed_time = bms_hardware.timer.elapsed_time
681        latest_v = bms_hardware.dmm.volts
682        latest_i = bms_hardware.charger.amps
683        max_charge_current = max(max_charge_current, latest_i)
684        logger.write_info_to_report(f"Elapsed Time: {elapsed_time}, Voltage: {latest_v}, Current: {latest_i}")
685
686        bms_hardware.csv.cycle.record(elapsed_time)
687        if current_time.elapsed_time > CHARGE_INC_TIME:
688            current_time.reset()
689            charge_current += CHARGE_INC_STEP
690        bms_hardware.charger.set_profile(volts=16.8, amps=charge_current)
691        time.sleep(charge_sample_interval)
692    bms_hardware.charger.disable()  # Charging is complete, turn off the charger
693    plateset.ce_switch = False
694    bms_hardware.csv.cycle = old_cycle_function

Increment current.

def test_cell_sim_voltage():
697def test_cell_sim_voltage():
698    """Set the cell sims to some voltage."""
699    # old_cycle_function = bms_hardware.csv.cycle
700    # bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
701    # bms_hardware.csv.cycle.create_file()
702
703    logger.write_info_to_report("Powering down cell sims")
704    for cell in bms_hardware.cells.values():
705        cell.disengage_safety_protocols = True
706        cell.volts = 0.0001
707    time.sleep(5)
708    logger.write_info_to_report("Powering up cell sims")
709    for cell in bms_hardware.cells.values():
710        cell.volts = CELL_VOLTAGE
711        cell.disengage_safety_protocols = False
712    # for cell in bms_hardware.cells.values():
713    #    cell.exact_volts = CELL_VOLTAGE
714    logger.write_info_to_report("Setting temperature to 15°C")
715    plateset.thermistor1 = DEFAULT_TEMPERATURE_C
716    plateset.thermistor2 = DEFAULT_TEMPERATURE_C
717
718    time.sleep(5)
719
720    standard_discharge(sample_interval=10, discharge_current=DISCHARGE_CURRENT / 1000, discharge_voltage=-0.05)
721
722    bms_hardware.resting_time = REST_TIME
723    bms_hardware.max_time = REST_TIME
724    bms_hardware.resting_sample_interval = REST_INTERVAL
725    bms_hardware.sample_interval = REST_INTERVAL
726    bms_hardware.run_resting_cycle()
727
728    # bms_hardware.csv.cycle = old_cycle_function

Set the cell sims to some voltage.

def test_cycle_count():
731def test_cycle_count():
732    """Repeat capacity test to confirm cycle count functionality."""
733    test_reset_cell_sims()
734
735    old_cycle_function = bms_hardware.csv.cycle
736    bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus
737    bms_hardware.csv.cycle.create_file()
738    # serial_watcher.start(csv=True)
739
740    # Change cell sim capacity
741    for cell in bms_hardware.cells.values():
742        cell.data.capacity = CYCLE_CAPACITY
743
744    # Run cycles
745    for i in range(CYCLE_COUNT):
746        logger.write_info_to_report(f"Running cycle {i}")
747        plateset.ce_switch = True  # Allows current charging in excess of 400 mA
748        standard_charge(termination_current=1.0)
749        plateset.ce_switch = False
750        standard_discharge(discharge_voltage=2.9 * 4)
751
752    # serial_watcher.stop()
753    bms_hardware.csv.cycle = old_cycle_function
754
755    # FIXME(JA): merge NGI branch to allow threads to access the same hardware
756    # assert serial_watcher.events["charge_cycles"][-1].value == CYCLE_COUNT

Repeat capacity test to confirm cycle count functionality.