hitl_tester.test_cases.bms.test_arts

Modified version of the tests in test_2590_cell_sims.py, for testing 17S and 20S arts energy packs.

Used in these test plans:

  • arts_20s_hitl_1_4 ⠀⠀⠀(bms/arts_20s_hitl_1_4.plan)
  • arts_17s_hitl_1_3 ⠀⠀⠀(bms/arts_17s_hitl_1_3.plan)

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

  • ./hitl_tester.py arts_20s_hitl_1_4 -DTYPE=17s -DCHARGE_VOLTAGE=28.9 -DCHARGE_OVERVOLTAGE=28.9 -DSAMPLE_INTERVAL=10 -DSTEP_START=1 -DSTEP_END=5 -DRECONDITION=False
  1"""
  2Modified version of the tests in test_2590_cell_sims.py, for testing 17S and 20S arts energy packs.
  3"""
  4
  5from __future__ import annotations
  6
  7from contextlib import suppress
  8
  9import pytest
 10
 11from hitl_tester.modules.bms.bms_hw import BMSHardware
 12from hitl_tester.modules.bms.plateset import Plateset
 13from hitl_tester.modules.bms_types import DischargeType, TimeoutExceededError
 14from hitl_tester.modules.logger import logger
 15
 16TYPE = "17s"  # 20s or 17s
 17CHARGE_VOLTAGE = 28.9 if TYPE == "17s" else 35
 18CHARGE_OVERVOLTAGE = 28.9 if TYPE == "17s" else 35  # 35V for the 20S and 28.9V for the 17S
 19SAMPLE_INTERVAL = 10
 20STEP_START = 1
 21STEP_END = 5
 22RECONDITION = False
 23
 24bms_hardware = BMSHardware(pytest.flags)  # type: ignore[arg-type]
 25bms_hardware.init()
 26
 27plateset = Plateset()
 28
 29test_step = pytest.mark.skipif(RECONDITION, reason="Running recondition cycles (skipping tests)")
 30recondition_step = pytest.mark.skipif(not RECONDITION, reason="Running test cycles (skipping recondition)")
 31
 32
 33def standard_charge(
 34    charge_current: float = 0.800,
 35    max_time: int = 16 * 3600,
 36    sample_interval: int = SAMPLE_INTERVAL,
 37    minimum_readings: int = 3,
 38    charge_voltage: float = CHARGE_VOLTAGE,
 39    termination_current: float = -0.05,  # Wait for timeout instead
 40):
 41    """Charge cells / pack."""
 42    bms_hardware.voltage = charge_voltage
 43    bms_hardware.ov_protection = CHARGE_OVERVOLTAGE
 44    bms_hardware.current = charge_current
 45    bms_hardware.termination_current = termination_current
 46    bms_hardware.max_time = max_time
 47    bms_hardware.sample_interval = sample_interval
 48    bms_hardware.minimum_readings = minimum_readings
 49
 50    # Run the Charge cycle
 51    bms_hardware.run_li_charge_cycle()
 52
 53
 54def standard_discharge(
 55    discharge_current: float = 0.800,
 56    max_time: int = 7 * 24 * 3600,
 57    sample_interval: int = SAMPLE_INTERVAL,
 58    discharge_voltage: float = 10,
 59    discharge_resistance: float = 10,
 60    discharge_type: DischargeType = DischargeType.CONSTANT_CURRENT,
 61):
 62    """Discharge cells / pack."""
 63    bms_hardware.voltage = discharge_voltage
 64    bms_hardware.uv_protection = bms_hardware.voltage - 0.100  # 100mV below voltage cutoff
 65    bms_hardware.current = discharge_current
 66    bms_hardware.discharge_type = discharge_type
 67    bms_hardware.max_time = max_time
 68    bms_hardware.sample_interval = sample_interval
 69    bms_hardware.resistance = discharge_resistance
 70
 71    # Run the discharge cycle, returning the capacity
 72    capacity = bms_hardware.run_discharge_cycle()
 73    logger.write_info_to_report(f"Discharge complete, capacity was {capacity * 1000.0} mAh")
 74    return capacity
 75
 76
 77@test_step
 78@pytest.mark.skipif(STEP_END < 1 or STEP_START > 1, reason=f"Starting on step {STEP_START}")
 79def test_step_1_arts():
 80    """
 81    Charge 800 mA, 50400 seconds
 82    Rest for 2 hours
 83    """
 84    with suppress(TimeoutExceededError):
 85        standard_charge(
 86            sample_interval=SAMPLE_INTERVAL,
 87            charge_current=0.800,
 88            termination_current=-0.05,
 89            charge_voltage=CHARGE_VOLTAGE,
 90            max_time=16 * 3600,
 91        )
 92
 93    bms_hardware.max_time = 2 * 3600  # 2 hours
 94    bms_hardware.sample_interval = SAMPLE_INTERVAL
 95    bms_hardware.run_resting_cycle()
 96
 97
 98@test_step
 99@pytest.mark.skipif(STEP_END < 1.5 or STEP_START > 1.5, reason=f"Starting on step {STEP_START}")
100def test_step_1_5_arts():
101    """Discharge at 17 / 20 ohms for 5 hours"""
102    with suppress(TimeoutExceededError):
103        standard_discharge(
104            sample_interval=SAMPLE_INTERVAL,
105            discharge_resistance=17 if TYPE == "17s" else 20,
106            discharge_type=DischargeType.CONSTANT_RESISTANCE,
107            discharge_voltage=-1,
108            max_time=5 * 3600,
109        )
110
111
112@test_step
113@pytest.mark.skipif(STEP_END < 2 or STEP_START > 2, reason=f"Starting on step {STEP_START}")
114def test_step_2_arts():
115    """
116    Charge 800 mA, 50400 seconds
117    Rest for 2 hours
118    Discharge at 3.4 / 4 ohms for 30 minutes
119    """
120    with suppress(TimeoutExceededError):
121        standard_charge(
122            sample_interval=SAMPLE_INTERVAL,
123            charge_current=0.800,
124            termination_current=-0.05,
125            charge_voltage=CHARGE_VOLTAGE,
126            max_time=16 * 3600,
127        )
128
129    bms_hardware.max_time = 2 * 3600  # 2 hours
130    bms_hardware.sample_interval = SAMPLE_INTERVAL
131    bms_hardware.run_resting_cycle()
132
133    with suppress(TimeoutExceededError):
134        standard_discharge(
135            sample_interval=SAMPLE_INTERVAL,
136            discharge_resistance=3.4 if TYPE == "17s" else 4,
137            discharge_type=DischargeType.CONSTANT_RESISTANCE,
138            discharge_voltage=-1,
139            max_time=1800,
140        )
141
142
143@test_step
144@pytest.mark.skipif(STEP_END < 3 or STEP_START > 3, reason=f"Starting on step {STEP_START}")
145def test_step_3_arts():
146    """
147    Charge 800 mA, 50400 seconds
148    Rest for 2 hours
149    Discharge at 2.04 / 2.4 ohms for 30 minutes
150    """
151    with suppress(TimeoutExceededError):
152        standard_charge(
153            sample_interval=SAMPLE_INTERVAL,
154            charge_current=0.800,
155            termination_current=-0.05,
156            charge_voltage=CHARGE_VOLTAGE,
157            max_time=16 * 3600,
158        )
159
160    bms_hardware.max_time = 2 * 3600  # 2 hours
161    bms_hardware.sample_interval = SAMPLE_INTERVAL
162    bms_hardware.run_resting_cycle()
163
164    with suppress(TimeoutExceededError):
165        standard_discharge(
166            sample_interval=SAMPLE_INTERVAL,
167            discharge_resistance=2.04 if TYPE == "17s" else 2.4,
168            discharge_type=DischargeType.CONSTANT_RESISTANCE,
169            discharge_voltage=-1,
170            max_time=1800,
171        )
172
173
174@test_step
175@pytest.mark.skipif(STEP_END < 4 or STEP_START > 4, reason=f"Starting on step {STEP_START}")
176def test_step_4_arts():
177    """
178    Charge at 800 mA for 50400 seconds
179    Let pack rest for 48 hours (Measure and record pack voltage after a 48-hour rest)
180    Measure pack impedance
181    Discharge at 800 mA down to 17V / 20V
182    """
183    # Charge
184    with suppress(TimeoutExceededError):
185        standard_charge(
186            sample_interval=SAMPLE_INTERVAL,
187            charge_current=0.800,
188            termination_current=-0.05,
189            charge_voltage=CHARGE_VOLTAGE,
190            max_time=16 * 3600,
191        )
192
193    # Rest
194    bms_hardware.max_time = 2 * 3600
195    bms_hardware.sample_interval = SAMPLE_INTERVAL
196    bms_hardware.run_resting_cycle()
197
198    # Measure impedance
199    pulse_current = 1.0
200    raw_voltage_data = bms_hardware.csv.ocv.measure_impedance_data(pulse_current)
201    impedance = bms_hardware.csv.ocv.calculate_impedance(raw_voltage_data, pulse_current)
202    bms_hardware.csv.raw_impedance.record(impedance, raw_voltage_data)
203    logger.write_result_to_html_report(f"Impedance: {impedance} mΩ")
204
205    # Discharge
206    standard_discharge(
207        sample_interval=SAMPLE_INTERVAL, discharge_current=0.800, discharge_voltage=17 if TYPE == "17s" else 20
208    )
209
210
211@recondition_step
212@pytest.mark.skipif(STEP_END < 1 or STEP_START > 1, reason=f"Starting on step {STEP_START}")
213def test_step_1_arts_recondition():
214    """
215    Discharge at 333 mA to 17V / 20V
216    """
217    standard_discharge(
218        sample_interval=SAMPLE_INTERVAL, discharge_current=0.333, discharge_voltage=17 if TYPE == "17s" else 20
219    )
220
221
222@recondition_step
223@pytest.mark.skipif(STEP_END < 2 or STEP_START > 2, reason=f"Starting on step {STEP_START}")
224def test_step_2_arts_recondition_step():
225    """
226    Charge 800 mA, 50400 seconds
227    """
228    with suppress(TimeoutExceededError):
229        standard_charge(
230            sample_interval=SAMPLE_INTERVAL,
231            charge_current=0.800,
232            termination_current=-0.05,
233            charge_voltage=CHARGE_VOLTAGE,
234            max_time=50400,
235        )
236
237
238@recondition_step
239@pytest.mark.skipif(STEP_END < 3 or STEP_START > 3, reason=f"Starting on step {STEP_START}")
240def test_step_3_arts_recondition():
241    """
242    Discharge at 333 mA to 17V / 20V
243    """
244    standard_discharge(
245        sample_interval=SAMPLE_INTERVAL, discharge_current=0.333, discharge_voltage=17 if TYPE == "17s" else 20
246    )
247
248
249@recondition_step
250@pytest.mark.skipif(STEP_END < 4 or STEP_START > 4, reason=f"Starting on step {STEP_START}")
251def test_step_4_arts_recondition_step():
252    """
253    Charge 800 mA, 50400 seconds
254    """
255    with suppress(TimeoutExceededError):
256        standard_charge(
257            sample_interval=SAMPLE_INTERVAL,
258            charge_current=0.800,
259            termination_current=-0.05,
260            charge_voltage=CHARGE_VOLTAGE,
261            max_time=50400,
262        )
263
264
265@recondition_step
266@pytest.mark.skipif(STEP_END < 5 or STEP_START > 5, reason=f"Starting on step {STEP_START}")
267def test_step_5_arts_recondition():
268    """
269    Discharge at 333 mA to 17V / 20V
270    """
271    standard_discharge(
272        sample_interval=SAMPLE_INTERVAL, discharge_current=0.333, discharge_voltage=17 if TYPE == "17s" else 20
273    )
TYPE = '17s'
CHARGE_VOLTAGE = 28.9
CHARGE_OVERVOLTAGE = 28.9
SAMPLE_INTERVAL = 10
STEP_START = 1
STEP_END = 5
RECONDITION = False
test_step = MarkDecorator(mark=Mark(name='skipif', args=(False,), kwargs={'reason': 'Running recondition cycles (skipping tests)'}))
recondition_step = MarkDecorator(mark=Mark(name='skipif', args=(True,), kwargs={'reason': 'Running test cycles (skipping recondition)'}))
def standard_charge( charge_current: float = 0.8, max_time: int = 57600, sample_interval: int = 10, minimum_readings: int = 3, charge_voltage: float = 28.9, termination_current: float = -0.05):
34def standard_charge(
35    charge_current: float = 0.800,
36    max_time: int = 16 * 3600,
37    sample_interval: int = SAMPLE_INTERVAL,
38    minimum_readings: int = 3,
39    charge_voltage: float = CHARGE_VOLTAGE,
40    termination_current: float = -0.05,  # Wait for timeout instead
41):
42    """Charge cells / pack."""
43    bms_hardware.voltage = charge_voltage
44    bms_hardware.ov_protection = CHARGE_OVERVOLTAGE
45    bms_hardware.current = charge_current
46    bms_hardware.termination_current = termination_current
47    bms_hardware.max_time = max_time
48    bms_hardware.sample_interval = sample_interval
49    bms_hardware.minimum_readings = minimum_readings
50
51    # Run the Charge cycle
52    bms_hardware.run_li_charge_cycle()

Charge cells / pack.

def standard_discharge( discharge_current: float = 0.8, max_time: int = 604800, sample_interval: int = 10, discharge_voltage: float = 10, discharge_resistance: float = 10, discharge_type: hitl_tester.modules.bms_types.DischargeType = <DischargeType.CONSTANT_CURRENT: 1>):
55def standard_discharge(
56    discharge_current: float = 0.800,
57    max_time: int = 7 * 24 * 3600,
58    sample_interval: int = SAMPLE_INTERVAL,
59    discharge_voltage: float = 10,
60    discharge_resistance: float = 10,
61    discharge_type: DischargeType = DischargeType.CONSTANT_CURRENT,
62):
63    """Discharge cells / pack."""
64    bms_hardware.voltage = discharge_voltage
65    bms_hardware.uv_protection = bms_hardware.voltage - 0.100  # 100mV below voltage cutoff
66    bms_hardware.current = discharge_current
67    bms_hardware.discharge_type = discharge_type
68    bms_hardware.max_time = max_time
69    bms_hardware.sample_interval = sample_interval
70    bms_hardware.resistance = discharge_resistance
71
72    # Run the discharge cycle, returning the capacity
73    capacity = bms_hardware.run_discharge_cycle()
74    logger.write_info_to_report(f"Discharge complete, capacity was {capacity * 1000.0} mAh")
75    return capacity

Discharge cells / pack.

@test_step
@pytest.mark.skipif(STEP_END < 1 or STEP_START > 1, reason=f'Starting on step {STEP_START}')
def test_step_1_arts():
78@test_step
79@pytest.mark.skipif(STEP_END < 1 or STEP_START > 1, reason=f"Starting on step {STEP_START}")
80def test_step_1_arts():
81    """
82    Charge 800 mA, 50400 seconds
83    Rest for 2 hours
84    """
85    with suppress(TimeoutExceededError):
86        standard_charge(
87            sample_interval=SAMPLE_INTERVAL,
88            charge_current=0.800,
89            termination_current=-0.05,
90            charge_voltage=CHARGE_VOLTAGE,
91            max_time=16 * 3600,
92        )
93
94    bms_hardware.max_time = 2 * 3600  # 2 hours
95    bms_hardware.sample_interval = SAMPLE_INTERVAL
96    bms_hardware.run_resting_cycle()

Charge 800 mA, 50400 seconds Rest for 2 hours

@test_step
@pytest.mark.skipif(STEP_END < 1.5 or STEP_START > 1.5, reason=f'Starting on step {STEP_START}')
def test_step_1_5_arts():
 99@test_step
100@pytest.mark.skipif(STEP_END < 1.5 or STEP_START > 1.5, reason=f"Starting on step {STEP_START}")
101def test_step_1_5_arts():
102    """Discharge at 17 / 20 ohms for 5 hours"""
103    with suppress(TimeoutExceededError):
104        standard_discharge(
105            sample_interval=SAMPLE_INTERVAL,
106            discharge_resistance=17 if TYPE == "17s" else 20,
107            discharge_type=DischargeType.CONSTANT_RESISTANCE,
108            discharge_voltage=-1,
109            max_time=5 * 3600,
110        )

Discharge at 17 / 20 ohms for 5 hours

@test_step
@pytest.mark.skipif(STEP_END < 2 or STEP_START > 2, reason=f'Starting on step {STEP_START}')
def test_step_2_arts():
113@test_step
114@pytest.mark.skipif(STEP_END < 2 or STEP_START > 2, reason=f"Starting on step {STEP_START}")
115def test_step_2_arts():
116    """
117    Charge 800 mA, 50400 seconds
118    Rest for 2 hours
119    Discharge at 3.4 / 4 ohms for 30 minutes
120    """
121    with suppress(TimeoutExceededError):
122        standard_charge(
123            sample_interval=SAMPLE_INTERVAL,
124            charge_current=0.800,
125            termination_current=-0.05,
126            charge_voltage=CHARGE_VOLTAGE,
127            max_time=16 * 3600,
128        )
129
130    bms_hardware.max_time = 2 * 3600  # 2 hours
131    bms_hardware.sample_interval = SAMPLE_INTERVAL
132    bms_hardware.run_resting_cycle()
133
134    with suppress(TimeoutExceededError):
135        standard_discharge(
136            sample_interval=SAMPLE_INTERVAL,
137            discharge_resistance=3.4 if TYPE == "17s" else 4,
138            discharge_type=DischargeType.CONSTANT_RESISTANCE,
139            discharge_voltage=-1,
140            max_time=1800,
141        )

Charge 800 mA, 50400 seconds Rest for 2 hours Discharge at 3.4 / 4 ohms for 30 minutes

@test_step
@pytest.mark.skipif(STEP_END < 3 or STEP_START > 3, reason=f'Starting on step {STEP_START}')
def test_step_3_arts():
144@test_step
145@pytest.mark.skipif(STEP_END < 3 or STEP_START > 3, reason=f"Starting on step {STEP_START}")
146def test_step_3_arts():
147    """
148    Charge 800 mA, 50400 seconds
149    Rest for 2 hours
150    Discharge at 2.04 / 2.4 ohms for 30 minutes
151    """
152    with suppress(TimeoutExceededError):
153        standard_charge(
154            sample_interval=SAMPLE_INTERVAL,
155            charge_current=0.800,
156            termination_current=-0.05,
157            charge_voltage=CHARGE_VOLTAGE,
158            max_time=16 * 3600,
159        )
160
161    bms_hardware.max_time = 2 * 3600  # 2 hours
162    bms_hardware.sample_interval = SAMPLE_INTERVAL
163    bms_hardware.run_resting_cycle()
164
165    with suppress(TimeoutExceededError):
166        standard_discharge(
167            sample_interval=SAMPLE_INTERVAL,
168            discharge_resistance=2.04 if TYPE == "17s" else 2.4,
169            discharge_type=DischargeType.CONSTANT_RESISTANCE,
170            discharge_voltage=-1,
171            max_time=1800,
172        )

Charge 800 mA, 50400 seconds Rest for 2 hours Discharge at 2.04 / 2.4 ohms for 30 minutes

@test_step
@pytest.mark.skipif(STEP_END < 4 or STEP_START > 4, reason=f'Starting on step {STEP_START}')
def test_step_4_arts():
175@test_step
176@pytest.mark.skipif(STEP_END < 4 or STEP_START > 4, reason=f"Starting on step {STEP_START}")
177def test_step_4_arts():
178    """
179    Charge at 800 mA for 50400 seconds
180    Let pack rest for 48 hours (Measure and record pack voltage after a 48-hour rest)
181    Measure pack impedance
182    Discharge at 800 mA down to 17V / 20V
183    """
184    # Charge
185    with suppress(TimeoutExceededError):
186        standard_charge(
187            sample_interval=SAMPLE_INTERVAL,
188            charge_current=0.800,
189            termination_current=-0.05,
190            charge_voltage=CHARGE_VOLTAGE,
191            max_time=16 * 3600,
192        )
193
194    # Rest
195    bms_hardware.max_time = 2 * 3600
196    bms_hardware.sample_interval = SAMPLE_INTERVAL
197    bms_hardware.run_resting_cycle()
198
199    # Measure impedance
200    pulse_current = 1.0
201    raw_voltage_data = bms_hardware.csv.ocv.measure_impedance_data(pulse_current)
202    impedance = bms_hardware.csv.ocv.calculate_impedance(raw_voltage_data, pulse_current)
203    bms_hardware.csv.raw_impedance.record(impedance, raw_voltage_data)
204    logger.write_result_to_html_report(f"Impedance: {impedance} mΩ")
205
206    # Discharge
207    standard_discharge(
208        sample_interval=SAMPLE_INTERVAL, discharge_current=0.800, discharge_voltage=17 if TYPE == "17s" else 20
209    )

Charge at 800 mA for 50400 seconds Let pack rest for 48 hours (Measure and record pack voltage after a 48-hour rest) Measure pack impedance Discharge at 800 mA down to 17V / 20V

@recondition_step
@pytest.mark.skipif(STEP_END < 1 or STEP_START > 1, reason=f'Starting on step {STEP_START}')
def test_step_1_arts_recondition():
212@recondition_step
213@pytest.mark.skipif(STEP_END < 1 or STEP_START > 1, reason=f"Starting on step {STEP_START}")
214def test_step_1_arts_recondition():
215    """
216    Discharge at 333 mA to 17V / 20V
217    """
218    standard_discharge(
219        sample_interval=SAMPLE_INTERVAL, discharge_current=0.333, discharge_voltage=17 if TYPE == "17s" else 20
220    )

Discharge at 333 mA to 17V / 20V

@recondition_step
@pytest.mark.skipif(STEP_END < 2 or STEP_START > 2, reason=f'Starting on step {STEP_START}')
def test_step_2_arts_recondition_step():
223@recondition_step
224@pytest.mark.skipif(STEP_END < 2 or STEP_START > 2, reason=f"Starting on step {STEP_START}")
225def test_step_2_arts_recondition_step():
226    """
227    Charge 800 mA, 50400 seconds
228    """
229    with suppress(TimeoutExceededError):
230        standard_charge(
231            sample_interval=SAMPLE_INTERVAL,
232            charge_current=0.800,
233            termination_current=-0.05,
234            charge_voltage=CHARGE_VOLTAGE,
235            max_time=50400,
236        )

Charge 800 mA, 50400 seconds

@recondition_step
@pytest.mark.skipif(STEP_END < 3 or STEP_START > 3, reason=f'Starting on step {STEP_START}')
def test_step_3_arts_recondition():
239@recondition_step
240@pytest.mark.skipif(STEP_END < 3 or STEP_START > 3, reason=f"Starting on step {STEP_START}")
241def test_step_3_arts_recondition():
242    """
243    Discharge at 333 mA to 17V / 20V
244    """
245    standard_discharge(
246        sample_interval=SAMPLE_INTERVAL, discharge_current=0.333, discharge_voltage=17 if TYPE == "17s" else 20
247    )

Discharge at 333 mA to 17V / 20V

@recondition_step
@pytest.mark.skipif(STEP_END < 4 or STEP_START > 4, reason=f'Starting on step {STEP_START}')
def test_step_4_arts_recondition_step():
250@recondition_step
251@pytest.mark.skipif(STEP_END < 4 or STEP_START > 4, reason=f"Starting on step {STEP_START}")
252def test_step_4_arts_recondition_step():
253    """
254    Charge 800 mA, 50400 seconds
255    """
256    with suppress(TimeoutExceededError):
257        standard_charge(
258            sample_interval=SAMPLE_INTERVAL,
259            charge_current=0.800,
260            termination_current=-0.05,
261            charge_voltage=CHARGE_VOLTAGE,
262            max_time=50400,
263        )

Charge 800 mA, 50400 seconds

@recondition_step
@pytest.mark.skipif(STEP_END < 5 or STEP_START > 5, reason=f'Starting on step {STEP_START}')
def test_step_5_arts_recondition():
266@recondition_step
267@pytest.mark.skipif(STEP_END < 5 or STEP_START > 5, reason=f"Starting on step {STEP_START}")
268def test_step_5_arts_recondition():
269    """
270    Discharge at 333 mA to 17V / 20V
271    """
272    standard_discharge(
273        sample_interval=SAMPLE_INTERVAL, discharge_current=0.333, discharge_voltage=17 if TYPE == "17s" else 20
274    )

Discharge at 333 mA to 17V / 20V