hitl_tester.test_cases.bms.test_flash_firmware

Test Flash the BMS
GitHub Issue(s) turnaroundfactor/HITL#374
Description Power on the cell sims, flash the board, and power down

Used in these test plans:

  • dev_bms ⠀⠀⠀(bms/dev_bms.plan)
  • flash_dev_b ⠀⠀⠀(bms/flash_dev_b.plan)
  • flash_dev_a ⠀⠀⠀(bms/flash_dev_a.plan)
  • flash_live_a ⠀⠀⠀(bms/flash_live_a.plan)
  • prod_bms ⠀⠀⠀(bms/prod_bms.plan)
  • flash_live_b ⠀⠀⠀(bms/flash_live_b.plan)
  • qa_bms ⠀⠀⠀(bms/qa_bms.plan)

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

  • ./hitl_tester.py dev_bms -DELF_PATH=. -DELF_FILE=battery-benchtop-rev1-barechip.elf -DBRANCH="" -DCAPACITY=0 -DDEFAULT_SOC_PERCENT=0.8 -DDEFAULT_TEMPERATURE_C=15 -DMAX_ATTEMPTS=5 -DHITL_DEBUG=0 -DBUILD=BB2590_v2_BOARD -DCONFIG_TEMPLATE=/opt/hostedtoolcache/Python/3.12.7/x64/lib/python3.12/site-packages/hitl_tester/test_cases/bms/flash_template.cfg
  1"""
  2| Test                 | Flash the BMS                                           |
  3| :------------------- | :------------------------------------------------------ |
  4| GitHub Issue(s)      | turnaroundfactor/HITL#374                        |
  5| Description          | Power on the cell sims, flash the board, and power down |
  6"""
  7
  8import shlex
  9import signal
 10import subprocess
 11import tempfile
 12import time
 13from pathlib import Path
 14from string import Template
 15
 16import pytest
 17
 18from hitl_tester.modules.bms.bms_hw import BMSHardware
 19from hitl_tester.modules.bms.plateset import Plateset
 20from hitl_tester.modules.bms_types import BMSFlags
 21from hitl_tester.modules.logger import logger
 22from hitl_tester.modules import properties
 23
 24ELF_PATH = Path(".")
 25"""Where to search for the elf file."""
 26
 27ELF_FILE = Path("battery-benchtop-rev1-barechip.elf")
 28"""The elf file to use."""
 29
 30BRANCH = ""
 31"""The name of the branch to build from, otherwise use local elf file."""
 32
 33CAPACITY = 0
 34"""The desired BMS capacity in mAh, otherwise default."""
 35
 36DEFAULT_SOC_PERCENT = 0.80
 37"""The cell SOC to start at if on cell sims."""
 38
 39DEFAULT_TEMPERATURE_C = 15
 40"""The thermistor temperature to start at if on cell sims."""
 41
 42MAX_ATTEMPTS = 5
 43"""How many flash attempts to make before failing."""
 44
 45HITL_DEBUG = 0
 46"""Puts the firmware into debug mode, lowering thresholds."""
 47
 48BUILD = "BB2590_v2_BOARD"
 49"""The board type to use."""
 50
 51CONFIG_TEMPLATE = Path(__file__).resolve().parent / "flash_template.cfg"  # Path relative to this file
 52"""The template for the flash config."""
 53
 54properties.apply()  # Allow modifying the above globals
 55_bms = BMSHardware(pytest.flags)  # type: ignore[arg-type]
 56_bms.init()
 57_plateset = Plateset()
 58
 59
 60def reset_cell_sims():
 61    """Activate cell sims and set appropriate temperatures."""
 62    logger.write_info_to_report("Setting temperature to 15°C")
 63    _plateset.thermistor1 = DEFAULT_TEMPERATURE_C
 64    _plateset.thermistor2 = DEFAULT_TEMPERATURE_C
 65    logger.write_info_to_report("Powering up cell sims")
 66    for cell in _bms.cells.values():
 67        cell.state_of_charge = DEFAULT_SOC_PERCENT
 68        cell.disengage_safety_protocols = False
 69
 70
 71def terminal_task(command: str, check: bool = False) -> str:
 72    """Execute a terminal command, outputting live text. and confirming it succeeded."""
 73    old_signal_handler = signal.getsignal(signal.SIGCHLD)
 74    signal.signal(signal.SIGCHLD, signal.SIG_DFL)
 75    output = ""
 76    with subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as process:
 77        if process.stdout is not None:
 78            for line in process.stdout:
 79                output += line
 80                if line := line.strip("\n"):
 81                    logger.write_info_to_report(f"  {line}")
 82        if process.wait() and check:
 83            logger.write_critical_to_report(f"[{process.returncode}] {process.stderr}")
 84            raise RuntimeError("Command failed")
 85    signal.signal(signal.SIGCHLD, old_signal_handler)
 86    return output
 87
 88
 89def execute_flash(config: Path | str):
 90    """Run the flash command."""
 91    command = f'openocd -s /usr/share/openocd/scripts -f {config} -c "program {ELF_PATH / ELF_FILE} verify reset exit"'
 92    logger.write_debug_to_report(f"Using command: {command}")
 93    return "Verified OK" in terminal_task(command)
 94
 95
 96def test_flash_firmware():
 97    """
 98    | Requirement          | Flash new firmware                                |
 99    | :------------------- | :------------------------------------------------ |
100    | GitHub Issue         | turnaroundfactor/HITL#216                  |
101    | Instructions         | 1. If a branch is provided, build and use it </br>\
102                             2. Activate the given ST-Link                </br>\
103                             3. Flash using openocd                       </br>\
104                             4. Deactivate the given ST-Link                   |
105    | Pass / Fail Criteria | Flash succeeded                                   |
106    | Estimated Duration   | 30 seconds                                        |
107    """
108    global ELF_PATH
109    reset_cell_sims()
110
111    # Checkout branch
112    if BRANCH:
113        firmware_path = Path("../battery-benchtop-rev1")
114        if not firmware_path.is_dir():
115            pytest.fail("Firmware repo is not installed. Cannot build ELF.")
116
117        fetch_cmd = f"git -C {firmware_path} fetch"
118        checkout_cmd = f"git -C {firmware_path} checkout {BRANCH}"
119        merge_cmd = f"git -C {firmware_path} merge"
120        build_cmake_cmd = (
121            f"cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_MAKE_PROGRAM=ninja -DCMAKE_C_COMPILER=/usr/bin/clang-11 "
122            f"-DCMAKE_CXX_COMPILER=/usr/bin/clang-cpp-11 -G Ninja {firmware_path} "
123            f"-B {firmware_path / 'cmake-build-debug'} -DBUILD={BUILD} -DCAPACITY={CAPACITY} "
124            f"-DHITL_DEBUG={HITL_DEBUG:d} -U CMAKE_C_COMPILER -U CMAKE_CXX_COMPILER"
125        )
126        build_elf_cmd = f"cmake --build {firmware_path / 'cmake-build-debug'} --target all -- -j 8"
127        logger.write_info_to_report("Fetching remote branches")
128        terminal_task(fetch_cmd, check=False)
129        logger.write_info_to_report(f"Checking out branch {BRANCH}")
130        terminal_task(checkout_cmd, check=False)
131        logger.write_info_to_report("Merging changes")
132        terminal_task(merge_cmd, check=False)
133        logger.write_info_to_report("Building...")
134        terminal_task(build_cmake_cmd, check=False)
135        terminal_task(build_elf_cmd, check=False)
136        ELF_PATH = firmware_path / "cmake-build-debug"
137
138    # Confirm that the files exist
139    if not Path(ELF_PATH / ELF_FILE).is_file():
140        raise FileNotFoundError(f"{ELF_PATH / ELF_FILE} does not exist.")
141    logger.write_info_to_report(f"ELF Filename: {ELF_PATH / ELF_FILE}")
142
143    # Create config file
144    assert hasattr(pytest, "flags") and isinstance(pytest.flags, BMSFlags)
145    serial_number = pytest.flags.config["st_link_serial"]
146    with tempfile.NamedTemporaryFile("w+", encoding="UTF-8") as temp_fp:
147        logger.write_info_to_report(f"Configuration Filename: {temp_fp.name}")
148        with open(CONFIG_TEMPLATE, encoding="UTF-8") as config_fp:
149            template = Template(config_fp.read())
150            temp_fp.write(template.substitute({"serial_number": serial_number}))
151            temp_fp.flush()
152        temp_fp.seek(0)
153        logger.write_debug_to_report(f"Template contents:\n{temp_fp.read()}")
154
155        # Attempt to flash
156        logger.write_info_to_report("Flashing...")
157        time.sleep(3)
158        for attempt in range(MAX_ATTEMPTS):
159            if execute_flash(temp_fp.name):
160                break
161            logger.write_error_to_report(f"Flashing failed. Retrying {attempt + 1}/{MAX_ATTEMPTS}.")
162            time.sleep(3)
163        else:
164            raise RuntimeError(f"Flashing failed after {MAX_ATTEMPTS} attempts.")
ELF_PATH = PosixPath('.')

Where to search for the elf file.

ELF_FILE = PosixPath('battery-benchtop-rev1-barechip.elf')

The elf file to use.

BRANCH = ''

The name of the branch to build from, otherwise use local elf file.

CAPACITY = 0

The desired BMS capacity in mAh, otherwise default.

DEFAULT_SOC_PERCENT = 0.8

The cell SOC to start at if on cell sims.

DEFAULT_TEMPERATURE_C = 15

The thermistor temperature to start at if on cell sims.

MAX_ATTEMPTS = 5

How many flash attempts to make before failing.

HITL_DEBUG = 0

Puts the firmware into debug mode, lowering thresholds.

BUILD = 'BB2590_v2_BOARD'

The board type to use.

CONFIG_TEMPLATE = PosixPath('/opt/hostedtoolcache/Python/3.12.7/x64/lib/python3.12/site-packages/hitl_tester/test_cases/bms/flash_template.cfg')

The template for the flash config.

def reset_cell_sims():
61def reset_cell_sims():
62    """Activate cell sims and set appropriate temperatures."""
63    logger.write_info_to_report("Setting temperature to 15°C")
64    _plateset.thermistor1 = DEFAULT_TEMPERATURE_C
65    _plateset.thermistor2 = DEFAULT_TEMPERATURE_C
66    logger.write_info_to_report("Powering up cell sims")
67    for cell in _bms.cells.values():
68        cell.state_of_charge = DEFAULT_SOC_PERCENT
69        cell.disengage_safety_protocols = False

Activate cell sims and set appropriate temperatures.

def terminal_task(command: str, check: bool = False) -> str:
72def terminal_task(command: str, check: bool = False) -> str:
73    """Execute a terminal command, outputting live text. and confirming it succeeded."""
74    old_signal_handler = signal.getsignal(signal.SIGCHLD)
75    signal.signal(signal.SIGCHLD, signal.SIG_DFL)
76    output = ""
77    with subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as process:
78        if process.stdout is not None:
79            for line in process.stdout:
80                output += line
81                if line := line.strip("\n"):
82                    logger.write_info_to_report(f"  {line}")
83        if process.wait() and check:
84            logger.write_critical_to_report(f"[{process.returncode}] {process.stderr}")
85            raise RuntimeError("Command failed")
86    signal.signal(signal.SIGCHLD, old_signal_handler)
87    return output

Execute a terminal command, outputting live text. and confirming it succeeded.

def execute_flash(config: pathlib.Path | str):
90def execute_flash(config: Path | str):
91    """Run the flash command."""
92    command = f'openocd -s /usr/share/openocd/scripts -f {config} -c "program {ELF_PATH / ELF_FILE} verify reset exit"'
93    logger.write_debug_to_report(f"Using command: {command}")
94    return "Verified OK" in terminal_task(command)

Run the flash command.

def test_flash_firmware():
 97def test_flash_firmware():
 98    """
 99    | Requirement          | Flash new firmware                                |
100    | :------------------- | :------------------------------------------------ |
101    | GitHub Issue         | turnaroundfactor/HITL#216                  |
102    | Instructions         | 1. If a branch is provided, build and use it </br>\
103                             2. Activate the given ST-Link                </br>\
104                             3. Flash using openocd                       </br>\
105                             4. Deactivate the given ST-Link                   |
106    | Pass / Fail Criteria | Flash succeeded                                   |
107    | Estimated Duration   | 30 seconds                                        |
108    """
109    global ELF_PATH
110    reset_cell_sims()
111
112    # Checkout branch
113    if BRANCH:
114        firmware_path = Path("../battery-benchtop-rev1")
115        if not firmware_path.is_dir():
116            pytest.fail("Firmware repo is not installed. Cannot build ELF.")
117
118        fetch_cmd = f"git -C {firmware_path} fetch"
119        checkout_cmd = f"git -C {firmware_path} checkout {BRANCH}"
120        merge_cmd = f"git -C {firmware_path} merge"
121        build_cmake_cmd = (
122            f"cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_MAKE_PROGRAM=ninja -DCMAKE_C_COMPILER=/usr/bin/clang-11 "
123            f"-DCMAKE_CXX_COMPILER=/usr/bin/clang-cpp-11 -G Ninja {firmware_path} "
124            f"-B {firmware_path / 'cmake-build-debug'} -DBUILD={BUILD} -DCAPACITY={CAPACITY} "
125            f"-DHITL_DEBUG={HITL_DEBUG:d} -U CMAKE_C_COMPILER -U CMAKE_CXX_COMPILER"
126        )
127        build_elf_cmd = f"cmake --build {firmware_path / 'cmake-build-debug'} --target all -- -j 8"
128        logger.write_info_to_report("Fetching remote branches")
129        terminal_task(fetch_cmd, check=False)
130        logger.write_info_to_report(f"Checking out branch {BRANCH}")
131        terminal_task(checkout_cmd, check=False)
132        logger.write_info_to_report("Merging changes")
133        terminal_task(merge_cmd, check=False)
134        logger.write_info_to_report("Building...")
135        terminal_task(build_cmake_cmd, check=False)
136        terminal_task(build_elf_cmd, check=False)
137        ELF_PATH = firmware_path / "cmake-build-debug"
138
139    # Confirm that the files exist
140    if not Path(ELF_PATH / ELF_FILE).is_file():
141        raise FileNotFoundError(f"{ELF_PATH / ELF_FILE} does not exist.")
142    logger.write_info_to_report(f"ELF Filename: {ELF_PATH / ELF_FILE}")
143
144    # Create config file
145    assert hasattr(pytest, "flags") and isinstance(pytest.flags, BMSFlags)
146    serial_number = pytest.flags.config["st_link_serial"]
147    with tempfile.NamedTemporaryFile("w+", encoding="UTF-8") as temp_fp:
148        logger.write_info_to_report(f"Configuration Filename: {temp_fp.name}")
149        with open(CONFIG_TEMPLATE, encoding="UTF-8") as config_fp:
150            template = Template(config_fp.read())
151            temp_fp.write(template.substitute({"serial_number": serial_number}))
152            temp_fp.flush()
153        temp_fp.seek(0)
154        logger.write_debug_to_report(f"Template contents:\n{temp_fp.read()}")
155
156        # Attempt to flash
157        logger.write_info_to_report("Flashing...")
158        time.sleep(3)
159        for attempt in range(MAX_ATTEMPTS):
160            if execute_flash(temp_fp.name):
161                break
162            logger.write_error_to_report(f"Flashing failed. Retrying {attempt + 1}/{MAX_ATTEMPTS}.")
163            time.sleep(3)
164        else:
165            raise RuntimeError(f"Flashing failed after {MAX_ATTEMPTS} attempts.")
Requirement Flash new firmware
GitHub Issue turnaroundfactor/HITL#216
Instructions 1. If a branch is provided, build and use it
2. Activate the given ST-Link
3. Flash using openocd
4. Deactivate the given ST-Link
Pass / Fail Criteria Flash succeeded
Estimated Duration 30 seconds