hitl_tester.test_cases.cyber_6t.mil_prf

Test SPEC Tests
GitHub Issue(s) turnaroundfactor/HITL#582
Description Tests for J1939 and MIL-PRF specifications.

(c) 2020-2024 TurnAround Factor, Inc.

#

CUI DISTRIBUTION CONTROL

Controlled by: DLA J68 R&D SBIP

CUI Category: Small Business Research and Technology

Distribution/Dissemination Controls: PROTECTED BY SBIR DATA RIGHTS

POC: GOV SBIP Program Manager Denise Price, 571-767-0111

Distribution authorized to U.S. Government Agencies only, to protect information not owned by the

U.S. Government and protected by a contractor’s ‘limited rights’ statement, or received with the understanding that

it is not routinely transmitted outside the U.S. Government (determination made September 14, 2024). Other requests

for this document shall be referred to ACOR DLA Logistics Operations (J-68), 8725 John J. Kingman Rd., Suite 4317,

Fort Belvoir, VA 22060-6221

#

SBIR DATA RIGHTS

Contract No.:SP4701-23-C-0083

Contractor Name: TurnAround Factor, Inc.

Contractor Address: 10365 Wood Park Ct. Suite 313 / Ashland, VA 23005

Expiration of SBIR Data Rights Period: September 24, 2029

The Government's rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer

software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights

in Noncommercial Technical Data and Computer Software--Small Business Innovative Research (SBIR) Program clause

contained in the above identified contract. No restrictions apply after the expiration date shown above. Any

reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce

the markings.

Used in these test plans:

  • mil_prf ⠀⠀⠀(cyber_6t/mil_prf.plan)

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

  • ./hitl_tester.py mil_prf -DBATTERY_CHANNEL=can0 -DHARD_RESET=False -DPROFILE="" -DFULL_MEMORY_TESTS=False -DADDRESS=232 -DBATTERY_INFO=namespace()
   1"""
   2| Test                 | SPEC Tests                                                   |
   3| :------------------- | :----------------------------------------------------------- |
   4| GitHub Issue(s)      | turnaroundfactor/HITL#582                             |
   5| Description          | Tests for J1939 and MIL-PRF specifications.                  |
   6
   7# (c) 2020-2024 TurnAround Factor, Inc.
   8#
   9# CUI DISTRIBUTION CONTROL
  10# Controlled by: DLA J68 R&D SBIP
  11# CUI Category: Small Business Research and Technology
  12# Distribution/Dissemination Controls: PROTECTED BY SBIR DATA RIGHTS
  13# POC: GOV SBIP Program Manager Denise Price, 571-767-0111
  14# Distribution authorized to U.S. Government Agencies only, to protect information not owned by the
  15# U.S. Government and protected by a contractor’s ‘limited rights’ statement, or received with the understanding that
  16# it is not routinely transmitted outside the U.S. Government (determination made September 14, 2024). Other requests
  17# for this document shall be referred to ACOR DLA Logistics Operations (J-68), 8725 John J. Kingman Rd., Suite 4317,
  18# Fort Belvoir, VA 22060-6221
  19#
  20# SBIR DATA RIGHTS
  21# Contract No.:SP4701-23-C-0083
  22# Contractor Name: TurnAround Factor, Inc.
  23# Contractor Address: 10365 Wood Park Ct. Suite 313 / Ashland, VA 23005
  24# Expiration of SBIR Data Rights Period: September 24, 2029
  25# The Government's rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer
  26# software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights
  27# in Noncommercial Technical Data and Computer Software--Small Business Innovative Research (SBIR) Program clause
  28# contained in the above identified contract. No restrictions apply after the expiration date shown above. Any
  29# reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce
  30# the markings.
  31"""
  32
  33from __future__ import annotations
  34
  35import itertools
  36import re
  37import time
  38from enum import Enum, IntEnum
  39from types import SimpleNamespace
  40from typing import Literal
  41
  42import pytest  # pylint: disable=wrong-import-order
  43from hitl_tester.modules.cyber_6t import spn_types
  44from hitl_tester.modules.cyber_6t.canbus import CANBus, CANFrame
  45from hitl_tester.modules.cyber_6t.j1939da import PGN
  46
  47from hitl_tester.modules import properties
  48from hitl_tester.modules.logger import logger
  49
  50
  51BATTERY_CHANNEL = "can0"
  52"""Channel identification. Expected type is backend dependent."""
  53
  54HARD_RESET = False
  55"""Whether to use a soft or hard reset before each test."""
  56
  57PROFILE = ""
  58"""The name of the profile to compare against. If this is not provided, a profile json will be generated."""
  59
  60FULL_MEMORY_TESTS = False  # TODO: Create variable in hitl_tester.py?
  61"""Short or long memory tests"""
  62
  63properties.apply()  # Allow modifying the above globals
  64
  65ADDRESS = 232
  66BATTERY_INFO = SimpleNamespace()
  67
  68
  69class ManufacturerID(IntEnum):
  70    """Enum to hold Manufacturer IDs."""
  71
  72    SAFT = 269  # NOTE: unused
  73    BRENTRONICS = 822
  74
  75
  76class Modes(float, Enum):
  77    """Enum for Command modes"""
  78
  79    ERASE = 0
  80    READ = 1
  81    WRITE = 2
  82    BOOT = 6
  83    EDCP = 7
  84
  85
  86class BadStates(Enum):
  87    """Enum for Bad Test Response States"""
  88
  89    NO_RESPONSE = 0
  90    WRONG_PGN = 1
  91    NACK = 2
  92    WRONG_PACKETS = 3
  93    INVALID_RESPONSE = 4
  94
  95
  96class Errors:
  97    """Holds different error messages"""
  98
  99    @staticmethod
 100    def timeout() -> None:
 101        """Prints message when connection times out"""
 102        message = "Could not locate 6T"
 103        logger.write_critical_to_report(message)
 104        pytest.exit(message)
 105
 106    @staticmethod
 107    def unexpected_packet(expected: str, frame: CANFrame) -> None:
 108        """Prints message when unexpected packet is received"""
 109        logger.write_warning_to_report(f"Expected {expected}, got {frame.pgn.short_name}")
 110
 111    @staticmethod
 112    def no_packet(expected: str) -> None:
 113        """Prints message when no packet is received"""
 114        logger.write_warning_to_report(f"Expected {expected}, got None")
 115
 116
 117# FIXME(JA): change issue to the prop issue / name issue
 118
 119
 120def cmp(
 121    a: float | str,
 122    sign: Literal["<", "<=", ">", ">=", "=="],
 123    b: float | str,
 124    unit_a: str = "",
 125    unit_b: str = "",
 126    form: str = "",
 127) -> str:
 128    """Generate a formatted string based on a comparison."""
 129    if not unit_b:
 130        unit_b = unit_a
 131
 132    if isinstance(a, str) or isinstance(b, str):
 133        return f"{a:{form}}{unit_a} {('≠', '=')[a == b]} {b:{form}}{unit_b}"
 134
 135    sign_str = {
 136        "<": ("≮", "<")[a < b],
 137        "<=": ("≰", "≤")[a <= b],
 138        ">": ("≯", ">")[a > b],
 139        ">=": ("≱", "≥")[a >= b],
 140        "==": ("≠", "=")[a == b],
 141    }
 142
 143    return f"{a:{form}}{unit_a} {sign_str[sign]} {b:{form}}{unit_b}"
 144
 145
 146@pytest.fixture(scope="class", autouse=True)
 147def reset_test_environment():
 148    """Before each test class, reset the 6T."""
 149
 150    try:
 151        power_cycle_frame = CANFrame(
 152            destination_address=BATTERY_INFO.address,
 153            pgn=PGN["PropA"],
 154            data=[0, 0, 1, 1, 1, not HARD_RESET, 1, 3, 0, -1],
 155        )
 156        maintenance_mode_frame = CANFrame(
 157            destination_address=BATTERY_INFO.address,
 158            pgn=PGN["PropA"],
 159            data=[0, 0, 1, 1, 1, 3, 1, 3, 0, -1],
 160        )
 161        factory_reset_frame = CANFrame(
 162            destination_address=BATTERY_INFO.address,
 163            pgn=PGN["PropA"],
 164            data=[1, 0, 0, -1, 3, 0xF, 3, 0, 0x1F, -1],
 165        )
 166        with CANBus(BATTERY_CHANNEL) as bus:
 167            logger.write_info_to_report("Power-Cycling 6T")
 168            bus.process_call(power_cycle_frame)
 169            time.sleep(10)
 170            logger.write_info_to_report("Entering maintenance mode")
 171            bus.process_call(maintenance_mode_frame)
 172            logger.write_info_to_report("Factory resetting 6T")
 173            bus.process_call(factory_reset_frame)
 174            time.sleep(10)
 175    except AttributeError:  # Battery has not yet been found
 176        return
 177
 178
 179class TestLocate6T:
 180    """Scan for 6T battery."""
 181
 182    def test_locate_6t(self) -> None:
 183        """
 184        | Description          | Scan the bus for devices                                         |
 185        | :------------------- | :--------------------------------------------------------------- |
 186        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#396                                 |
 187        | Instructions         | 1. Request the names of everyone on the bus                 </br>\
 188                                 2. Use the response data for all communication                   |
 189        | Estimated Duration   | 1 second                                                         |
 190        """
 191        name_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id])
 192        with CANBus(BATTERY_CHANNEL) as bus:
 193            if name_frame := bus.process_call(name_request):
 194                # Save responses to profile
 195                BATTERY_INFO.id = int(name_frame.data[0])
 196                BATTERY_INFO.manufacturer_code = name_frame.data[1]
 197                if (mfg_name := spn_types.manufacturer_id(name_frame.data[1])) == "Reserved":
 198                    mfg_name = f"Unknown ({name_frame.data[1]})"
 199                BATTERY_INFO.manufacturer_name = mfg_name
 200                BATTERY_INFO.address = name_frame.source_address
 201            else:
 202                message = "Could not locate 6T"
 203                logger.write_warning_to_html_report(message)
 204                pytest.exit(message)
 205
 206        # Log results
 207        printable_id = "".join(
 208            chr(i) if chr(i).isprintable() else "." for i in BATTERY_INFO.id.to_bytes(3, byteorder="big")
 209        )
 210        logger.write_result_to_html_report(
 211            f"Found {BATTERY_INFO.manufacturer_name} 6T at address {BATTERY_INFO.address} "
 212            f"(ID: {BATTERY_INFO.id:06X}, ASCII ID: {printable_id})"
 213        )
 214
 215
 216class TestNameInformation:
 217    """Check Unique ID (Identity Number) field in Address Claimed message"""
 218
 219    def test_name_information(self) -> None:
 220        """
 221        | Description          | Confirm information in NAME (address claimed) matches            \
 222                                 expected value.                                                  |
 223        | :------------------- | :--------------------------------------------------------------- |
 224        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#396                                 |
 225        | Instructions         | 1. Request Address Claimed data                             </br>\
 226                                 2. Log returned values                                      </br>\
 227                                 3. Validate if values meet spec requirements                     |
 228        | Estimated Duration   | 1 second                                                         |
 229        """
 230
 231        name_values = {}
 232        failed_values = []
 233
 234        name_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id])
 235        with CANBus(BATTERY_CHANNEL) as bus:
 236            if name_frame := bus.process_call(name_request):
 237                name_values["identity_number"] = int(name_frame.data[0])
 238                vehicle_system_instance = 0
 239
 240                for spn, elem in zip(PGN["Address Claimed"].data_field, name_frame.data):
 241                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
 242                    logger.write_info_to_report(f"{spn.name}: {elem} {description}")
 243
 244                    if spn.name in ("Manufacturer Code", "Reserved"):
 245                        continue
 246
 247                    high_range = 0
 248                    if spn.name == "Identity Number":
 249                        high_range = 2097151
 250
 251                    if spn.name == "ECU Instance":
 252                        high_range = 7
 253
 254                    if spn.name == "Function Instance":
 255                        high_range = 31
 256
 257                    if spn.name == "Function":
 258                        high_range = 254
 259
 260                    if spn.name == "Vehicle System Instance":
 261                        high_range = 15
 262                        vehicle_system_instance = elem
 263
 264                    if spn.name == "Industry Group":
 265                        high_range = 7
 266
 267                    if spn.name == "Arbitrary Address Capable":
 268                        high_range = 1
 269
 270                    if spn.name == "Vehicle System":
 271                        if elem not in (127, 0, vehicle_system_instance):
 272                            logger.write_warning_to_html_report(
 273                                f"Vehicle System {elem} is not 127, 0, or the same value as " f"Vehicle System Instance"
 274                            )
 275                            failed_values.append(spn.name)
 276                    else:
 277                        if not 0 <= elem <= high_range:
 278                            logger.write_warning_to_html_report(
 279                                f"{spn.name}: {cmp(elem, '>=', 0)} and " f"{cmp(elem, '<=', high_range)}"
 280                            )
 281                            failed_values.append(spn.name)
 282
 283            else:
 284                message = f"No response to PGN {PGN['Address Claimed', [32]].id} (Address Claimed)"
 285                logger.write_failure_to_html_report(message)
 286                pytest.fail(message)
 287
 288        if len(failed_values) > 0:
 289            categories = "data values" if len(failed_values) > 1 else "data value"
 290            message = (
 291                f"{len(failed_values)} {categories} failed Address Claimed specifications: {', '.join(failed_values)}"
 292            )
 293            logger.write_failure_to_html_report(message)
 294            pytest.fail(message)
 295
 296        logger.write_result_to_html_report(
 297            f"Found Identity Number: {name_values['identity_number']} (0x{name_values['identity_number']:06x}) "
 298            f"for {BATTERY_INFO.manufacturer_name} 6T at address {BATTERY_INFO.address}"
 299        )
 300
 301
 302class TestSoftwareIdentificationInformation:
 303    """Retrieve & Validate Software Identification Information"""
 304
 305    def test_software_identification_information(self) -> None:
 306        """
 307        | Description          | Validate Software Identification                                 |
 308        | :------------------- | :--------------------------------------------------------------- |
 309        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#394                                 |
 310        | Instructions         | 1. Request Software Identification                          </br>\
 311                                 2. Validate Software Identification                         </br>\
 312                                 3. Log Response                                                  |
 313        | Estimated Duration   | 1 second                                                         |
 314        """
 315
 316        soft_request = CANFrame(pgn=PGN["RQST"], data=[0xFEDA])
 317        soft_data = []
 318        with CANBus(BATTERY_CHANNEL) as bus:
 319            if soft_frame := bus.process_call(soft_request):
 320                # Complete RTS
 321                if len(soft_frame.data) < 3:
 322                    message = (
 323                        f"Unexpected byte response from PGN "
 324                        f"{PGN['Software Identification', [32]].id} (Software Identification)"
 325                    )
 326                    logger.write_warning_to_html_report(message)
 327                    return
 328                expected_bytes = int(soft_frame.data[1])
 329                expected_packets = 0
 330                if BATTERY_INFO.manufacturer_code == ManufacturerID.BRENTRONICS:
 331                    expected_packets = int(soft_frame.data[2] + 1)
 332                else:
 333                    expected_packets = int(soft_frame.data[2])
 334
 335                rts_pgn_id = int(soft_frame.data[-1])
 336                cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0])
 337                cts_request.data[1] = expected_packets
 338                cts_request.data[-1] = rts_pgn_id
 339
 340                # Send CTS
 341                bus.send_message(cts_request.message())
 342
 343                # Read & Store data from packets
 344                for i in range(expected_packets):
 345                    data_frame = bus.read_frame()
 346                    if data_frame.pgn.id != 0xEB00:
 347                        message = f"Expected data frame {i + 1}, got PGN {data_frame.pgn.id}"
 348                        logger.write_failure_to_html_report(message)
 349                        pytest.fail(message)
 350                    soft_data.append(hex(int(data_frame.data[1])))
 351
 352                if len(soft_data) != expected_packets:
 353                    message = f"Expected {expected_packets} packets, got {len(soft_data)}"
 354                    logger.write_failure_to_html_report(message)
 355                    pytest.fail(message)
 356
 357                # Send acknowledgement frame
 358                end_acknowledge_frame = CANFrame(pgn=PGN["TP.CM"], data=[19, 0, 0, 0xFF, 0])
 359                end_acknowledge_frame.data[1] = expected_bytes
 360                end_acknowledge_frame.data[2] = expected_packets
 361                end_acknowledge_frame.data[-1] = rts_pgn_id
 362
 363                bus.send_message(end_acknowledge_frame.message())
 364            else:
 365                message = f"No response for PGN {PGN['Software Identification', [32]].id} (Software Identification)"
 366                logger.write_warning_to_html_report(message)
 367                return
 368
 369        hex_string = "0x"
 370        add_to_data = False
 371        finish_adding = False
 372
 373        # Find Software Identification Number
 374        for element in soft_data:
 375            n = len(element) - 1
 376            while n >= 1:
 377                if add_to_data:
 378                    if element[n - 1 : n + 1] == "2a":
 379                        finish_adding = True
 380                        break
 381                    if element[n - 1 : n + 1] != "0x":
 382                        hex_string += element[n - 1 : n + 1]
 383                        n -= 2
 384                    else:
 385                        n -= 2
 386                else:
 387                    if element[n - 3 : n + 1] == "2a31" or element[n - 3 : n + 1] == "2a01":
 388                        add_to_data = True
 389                        n -= 4
 390                    else:
 391                        n -= 4
 392            if finish_adding:
 393                break
 394
 395        if len(hex_string) <= 2:
 396            logger.write_warning_to_html_report(
 397                f"Software Identification: {hex_string} was not found in expected format"
 398            )
 399            return
 400
 401        software_identification = spn_types.ascii_map(int(hex_string, 16))
 402
 403        if re.search(r"[0-9]{2}\.[0-9]{2}\.[a-z]{2}\.[a-z]{2,}", software_identification) is None:
 404            message = f"Software Identification: {software_identification} was not in expected format: MM.II.mm.aa.ee"
 405            logger.write_failure_to_html_report(message)
 406            pytest.fail(message)
 407
 408        logger.write_result_to_html_report(f"Software Identification: {software_identification}")
 409
 410
 411class TestBatteryRegulationInformation:
 412    """Retrieve and validate battery regulation information"""
 413
 414    def test_battery_regulation_information(self) -> None:
 415        """
 416        | Description          | Validate Battery Regulation Information 1 & 2                    |
 417        | :------------------- | :--------------------------------------------------------------- |
 418        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#392                                 |
 419        | Instructions         | 1. Request Battery Regulation Information 1                 </br>\
 420                                 2. Validate Information                                     </br>\
 421                                 3. Log Response                                                  |
 422        | Estimated Duration   | 2 seconds                                                        |
 423        """
 424
 425        battery_regulation_one_request = CANFrame(pgn=PGN["RQST"], data=[0xFF03])
 426        battery_regulation_two_request = CANFrame(pgn=PGN["RQST"], data=[0xFF04])
 427
 428        # Obtain Battery Regulation One Information
 429        with CANBus(BATTERY_CHANNEL) as bus:
 430            logger.write_result_to_html_report(
 431                "<span style='font-weight: bold'>Battery Regulation Information 1 </span>"
 432            )
 433            if battery_frame := bus.process_call(battery_regulation_one_request):
 434
 435                if battery_frame.pgn.id != PGN["PropB_03", [32]].id:
 436                    message = f"Expected {PGN['PropB_03', [32]].id} 0xFF03, but received PGN {battery_frame.pgn.id}"
 437                    logger.write_warning_to_html_report(message)
 438
 439                for spn, elem in zip(PGN["PropB_03"].data_field, battery_frame.data):
 440                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
 441                    logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
 442
 443                    high_range = 3212.75
 444                    low_range = 0.0
 445                    if spn.name == "Battery Current":
 446                        high_range = 1600.00
 447                        low_range = -1600.00
 448
 449                    if not low_range <= elem <= high_range:
 450                        logger.write_warning_to_html_report(
 451                            f"{spn.name}: {cmp(elem, '>=', low_range)} and " f"{cmp(elem, '<=', high_range)}"
 452                        )
 453            else:
 454                message = f"Did not receive response for PGN {PGN['PropB_03', [32]].id} (Battery Regulation One)"
 455                logger.write_warning_to_html_report(message)
 456
 457        # Obtain Battery Regulation Two Information
 458        with CANBus(BATTERY_CHANNEL) as bus:
 459            logger.write_result_to_html_report(
 460                "<span style='font-weight: bold'>Battery Regulation Information 2</span>"
 461            )
 462            if battery_frame_two := bus.process_call(battery_regulation_two_request):
 463
 464                if battery_frame_two.pgn.id != PGN["PropB_04", [32]].id:
 465                    message = f"Expected PGN {PGN['PropB_04', [32]].id}, but received PGN {battery_frame_two.pgn.id}"
 466                    logger.write_warning_to_html_report(message)
 467
 468                for spn, elem in zip(PGN["PropB_04"].data_field, battery_frame_two.data):
 469
 470                    if spn.name == "Reserved":
 471                        continue
 472
 473                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
 474                    logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
 475
 476                    high_range = 3
 477                    if spn.name == "Bus Voltage Request":
 478                        high_range = 3212.75
 479
 480                    if spn.name == "Transportability SOC":
 481                        high_range = 100
 482
 483                    if not 0 <= elem <= high_range:
 484                        logger.write_warning_to_html_report(
 485                            f"{spn.name}: {cmp(elem, '>=', 0)} and {cmp(elem, '<=', high_range)}"
 486                        )
 487
 488            else:
 489                message = f"Did not receive response for PGN {PGN['PropB_04', [32]].id} (Battery Regulation Two)"
 490                logger.write_warning_to_html_report(message)
 491
 492
 493class TestConfigurationStateMessage:
 494    """Confirms Configuration State Message matches profile after factory reset"""
 495
 496    def test_configuration_state_message(self) -> None:
 497        """
 498        | Description          | Validates if Configuration State Message matches profile         |
 499        | :------------------- | :--------------------------------------------------------------- |
 500        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#393                                 |
 501        | Instructions         | 1. Request Configuration State                              </br>\
 502                                 2. Check if Configuration State matches default values      </br>\
 503                                 3. Log results                                                   |
 504        | Estimated Duration   | 1 second                                                         |
 505        """
 506        failed_values = []
 507        configuration_state_request = CANFrame(pgn=PGN["Request"], data=[PGN["Configuration State Message 1"].id])
 508        with CANBus(BATTERY_CHANNEL) as bus:
 509            if configuration_frame := bus.process_call(configuration_state_request):
 510                if configuration_frame.pgn.id != PGN["Configuration State Message 1", [32]].id:
 511                    message = (
 512                        f"Received PGN {configuration_frame.pgn.id}, not PGN "
 513                        f"{PGN['Configuration State Message 1', [32]].id} "
 514                    )
 515                    logger.write_failure_to_html_report(message)
 516                    pytest.fail(message)
 517
 518                data = configuration_frame.data
 519                pgn_data_field = PGN["Configuration State Message 1"].data_field
 520
 521                if len(data) != len(pgn_data_field):
 522                    message = (
 523                        f"Expected {len(pgn_data_field)} data fields, "
 524                        f"got {len(data)}. Data is missing from response"
 525                    )
 526                    logger.write_failure_to_html_report(message)
 527                    pytest.fail(message)
 528
 529                logger.write_result_to_html_report("<span style='font-weight: bold'>Configuration State Message</span>")
 530
 531                for spn, elem in zip(pgn_data_field, data):
 532                    if spn.name == "Reserved":
 533                        continue
 534
 535                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
 536                    logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
 537
 538                    if not 0 <= elem <= 3:
 539                        message = (
 540                            f"{spn.name}: {cmp(elem, '>=', 0, f'({spn.data_type(elem)})')} and "
 541                            f"{cmp(elem, '<=', 3, f'({spn.data_type(elem)})')}"
 542                        )
 543                        logger.write_warning_to_html_report(message)
 544                        failed_values.append(spn.name)
 545                        continue
 546
 547                    if spn.name in ("Battery Battle-override State", "Standby State", "Configure VPMS Function State"):
 548                        message = (
 549                            f"{spn.name}: {cmp(elem, '==', 0, f'({spn.data_type(elem)})', f'({spn.data_type(0)})')}"
 550                        )
 551                        if elem != 0:
 552                            logger.write_warning_to_html_report(message)
 553                            failed_values.append(spn.name)
 554
 555                    if spn.name in ("Automated Heater Function State", "Contactor(s) Control State"):
 556                        message = (
 557                            f"{spn.name}: {cmp(elem, '==', 1, f'({spn.data_type(elem)})', f'({spn.data_type(1)})')}"
 558                        )
 559                        if elem != 1:
 560                            logger.write_warning_to_html_report(message)
 561                            failed_values.append(spn.name)
 562
 563                if len(failed_values) > 0:
 564                    categories = "data values" if len(failed_values) > 1 else "data value"
 565                    message = (
 566                        f"{len(failed_values)} {categories} failed "
 567                        f"Configuration State Message specifications: {', '.join(failed_values)}"
 568                    )
 569                    logger.write_failure_to_html_report(message)
 570                    pytest.fail(message)
 571
 572            else:
 573                message = (
 574                    f"Did not receive a response for PGN {PGN['Configuration State Message 1', [32]].id} "
 575                    f"(Configuration State Message 1)"
 576                )
 577                logger.write_failure_to_html_report(message)
 578                pytest.fail(message)
 579
 580
 581def na_maintenance_mode(bus: CANBus) -> bool:
 582    """This function will place battery in Maintenance Mode with Reset Value of "3"."""
 583
 584    maintenance_mode = CANFrame(  # Memory access only works in maintenance mode
 585        destination_address=BATTERY_INFO.address,
 586        pgn=PGN["PropA"],
 587        data=[0, 0, 1, 1, 1, 3, 1, 3, 0, -1],
 588    )
 589
 590    bus.send_message(maintenance_mode.message())  # Enter maintenance mode
 591    if response := bus.read_input():
 592        ack_frame = CANFrame.decode(response.arbitration_id, response.data)
 593        if ack_frame.pgn.id != 0xE800 and ack_frame.data[0] != 0:
 594            logger.write_warning_to_html_report("Unable to enter maintenance mode")
 595            return False
 596        return True
 597
 598    message = "Received no maintenance mode response"
 599    logger.write_warning_to_html_report(message)
 600    return False
 601
 602
 603class TestNameManagementMessage:
 604    """This will test the mandatory NAME Management commands"""
 605
 606    def test_name_management_command(self) -> None:
 607        """
 608        | Description          | Test Name Management Command                                     |
 609        | :------------------- | :--------------------------------------------------------------- |
 610        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
 611        | Instructions         | 1. Get NAME information from Address Claimed                </br>\
 612                                 2. Change ECU value                                         </br>\
 613                                 3. Test Name Management Command                             </br>\
 614                                 4. Check Values updated                                     </br>\
 615                                 5. Log results                                                   |
 616        | Estimated Duration   | 1 second                                                         |
 617        """
 618
 619        old_ecu_value = 0
 620        new_ecu_value = 0
 621        checksum_value = 0
 622        adopt_name_request_data: list[float] = [255, 1, 1, 1, 1, 1, 1, 1, 1, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
 623        address_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id])
 624
 625        with CANBus(BATTERY_CHANNEL) as bus:
 626            na_maintenance_mode(bus)
 627
 628            # Name Management Test
 629            if address_claimed := bus.process_call(address_request):
 630                if address_claimed.pgn.id != PGN["Address Claimed", [32]].id:
 631                    Errors.unexpected_packet("Address Claimed", address_claimed)
 632                    message = f"Unexpected packet PGN {address_claimed.pgn.id} was received"
 633                    logger.write_failure_to_html_report(message)
 634                    pytest.fail(message)
 635
 636                checksum_value = sum(address_claimed.packed_data) & 0xFF
 637                old_ecu_value = address_claimed.data[2]
 638                if old_ecu_value > 0:
 639                    new_ecu_value = 0
 640                else:
 641                    new_ecu_value = 1
 642            else:
 643                message = f"No response received for PGN {[PGN['Address Claimed', [32]].id]}"
 644                logger.write_result_to_html_report(message)
 645
 646            if not checksum_value:
 647                message = "Could not condense Name into bits for NAME Management"
 648                logger.write_failure_to_html_report(message)
 649                pytest.fail(message)
 650
 651            request_data: list[float] = [
 652                checksum_value,
 653                1,
 654                0,
 655                1,
 656                1,
 657                1,
 658                1,
 659                1,
 660                1,
 661                0,
 662                1,
 663                1,
 664                new_ecu_value,
 665                1,
 666                1,
 667                1,
 668                1,
 669                1,
 670                1,
 671                1,
 672            ]
 673            name_management_set_name = CANFrame(
 674                destination_address=BATTERY_INFO.address, pgn=PGN["NAME Management Message"], data=request_data
 675            )
 676
 677            if pending_response := bus.process_call(name_management_set_name):
 678                if pending_response.pgn.id != PGN["NAME Management Message", [32]].id:
 679                    if pending_response.pgn.id == PGN["Acknowledgement", [32]].id:
 680                        message = (
 681                            f"Battery may not support NAME Management, received PGN "
 682                            f"{pending_response.pgn.id} ({spn_types.acknowledgement(pending_response.data[0])})"
 683                        )
 684                        logger.write_failure_to_html_report(message)
 685                    else:
 686                        Errors.unexpected_packet("NAME Management Message", pending_response)
 687                        message = f"Unexpected PGN {pending_response.pgn.id} received"
 688                        logger.write_failure_to_html_report(message)
 689                    pytest.fail(message)
 690
 691                if pending_response.data[0] == 3:
 692                    message = (
 693                        f"Message was unsuccessful, received error: "
 694                        f"{spn_types.name_error_code(pending_response.data[0])}"
 695                    )
 696                    logger.write_failure_to_html_report(message)
 697                    pytest.fail(message)
 698
 699                if pending_response.data[12] != new_ecu_value:
 700                    message = f"ECU Value was not changed from {old_ecu_value} to {new_ecu_value}"
 701                    logger.write_failure_to_html_report(message)
 702                    pytest.fail(message)
 703            else:
 704                message = (
 705                    f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)"
 706                )
 707                logger.write_failure_to_html_report(message)
 708                pytest.fail(message)
 709
 710            adopt_name_request = CANFrame(
 711                destination_address=BATTERY_INFO.address,
 712                pgn=PGN["NAME Management Message"],
 713                data=adopt_name_request_data,
 714            )
 715
 716            bus.send_message(adopt_name_request.message())
 717
 718            current_name_request_data: list[float] = [255, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
 719            current_name_request = CANFrame(
 720                destination_address=BATTERY_INFO.address,
 721                pgn=PGN["NAME Management Message"],
 722                data=current_name_request_data,
 723            )
 724
 725            if name_management_response := bus.process_call(current_name_request):
 726                if name_management_response.data[12] != new_ecu_value:
 727                    message = (
 728                        f"Name's ECU Instance was not updated after Name Management Changes, "
 729                        f"{cmp(name_management_response.data[12], '==', new_ecu_value)}"
 730                    )
 731                    logger.write_failure_to_html_report(message)
 732                    pytest.fail(message)
 733            else:
 734                message = (
 735                    f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)"
 736                )
 737                logger.write_failure_to_html_report(message)
 738                pytest.fail(message)
 739
 740            if new_address_claimed := bus.process_call(address_request):
 741                if new_address_claimed.data[2] != new_ecu_value + 1:
 742                    message = (
 743                        f"Address Claimed was not updated after Name Management Changes. "
 744                        f"{cmp(new_address_claimed.data[2], '==', new_ecu_value + 1)}"
 745                    )
 746                    logger.write_failure_to_html_report(message)
 747                    pytest.fail(message)
 748
 749            logger.write_result_to_html_report(
 750                f"Name Management Command was successful. "
 751                f"ECU Instance was changed from {old_ecu_value} to {new_ecu_value}"
 752            )
 753
 754    def test_wrong_name_management_data(self):
 755        """
 756        | Description          | Test Incorrect Data for NAME Management Command                  |
 757        | :------------------- | :--------------------------------------------------------------- |
 758        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
 759        | Instructions         | 1. Provide wrong Checksum value for Name Command            </br>\
 760                                 2. Check receive Checksum error code as response            </br>\
 761                                 3. Log results                                                   |
 762        | Estimated Duration   | 1 second                                                         |
 763        """
 764        wrong_checksum = 0
 765        new_ecu_value = 0
 766
 767        current_name_request_data = [255, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
 768        current_name_request = CANFrame(
 769            destination_address=BATTERY_INFO.address,
 770            pgn=PGN["NAME Management Message"],
 771            data=current_name_request_data,
 772        )
 773
 774        with CANBus(BATTERY_CHANNEL) as bus:
 775            # Test Checksum Error -- Should return Error Code: 3
 776            na_maintenance_mode(bus)
 777
 778            if current_name := bus.process_call(current_name_request):
 779                if current_name.pgn.id != PGN["NAME Management Message", [32]].id:
 780                    if current_name.pgn.id == PGN["Acknowledgement", [32]].id:
 781                        message = (
 782                            f"Battery may not support NAME Management, received PGN "
 783                            f"{current_name.pgn.id} ({spn_types.acknowledgement(current_name.data[0])})"
 784                        )
 785                        logger.write_failure_to_html_report(message)
 786                        pytest.fail(message)
 787                    Errors.unexpected_packet("NAME Management Message", current_name)
 788                    message = f"Unexpected packet was received: PGN {current_name.pgn.id}"
 789                    logger.write_failure_to_html_report(message)
 790                    pytest.fail(message)
 791
 792                wrong_checksum = sum(current_name.packed_data) & 0xFF
 793                old_ecu_value = current_name.data[12]
 794                if old_ecu_value > 0:
 795                    new_ecu_value = 0
 796                else:
 797                    new_ecu_value = 1
 798            else:
 799                message = (
 800                    f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)"
 801                )
 802                logger.write_failure_to_html_report(message)
 803                pytest.fail(message)
 804
 805            request_data = [wrong_checksum, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, new_ecu_value, 1, 1, 1, 1, 1, 1, 1]
 806            name_management_set_name = CANFrame(
 807                destination_address=BATTERY_INFO.address, pgn=PGN["NAME Management Message"], data=request_data
 808            )
 809
 810            if pending_response := bus.process_call(name_management_set_name):
 811                if pending_response.pgn.id != PGN["NAME Management Message", [32]].id:
 812                    if pending_response.pgn.id == PGN["Acknowledgement", [32]].id:
 813                        message = (
 814                            f"Battery may not support NAME Management, received PGN {pending_response.pgn.id} "
 815                            f"({spn_types.acknowledgement(pending_response.data[0])})"
 816                        )
 817                        logger.write_failure_to_html_report(message)
 818                        pytest.fail(message)
 819                    Errors.unexpected_packet("NAME Management Message", pending_response)
 820                    message = f"Unexpected packet was received: {pending_response.pgn.id}"
 821                    logger.write_failure_to_html_report(message)
 822                    pytest.fail(message)
 823
 824                if pending_response.data[0] != 3:
 825                    message = cmp(
 826                        pending_response.data[0],
 827                        "==",
 828                        3,
 829                        f"({spn_types.name_error_code(pending_response.data[0])})",
 830                        f"({spn_types.name_error_code(3)})",
 831                    )
 832                    logger.write_failure_to_html_report(
 833                        f"PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message) "
 834                        f"updated NAME Management with incorrect checksum value"
 835                    )
 836                    logger.write_failure_to_html_report(message)
 837                    pytest.fail(message)
 838            else:
 839                message = (
 840                    f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)"
 841                )
 842                logger.write_failure_to_html_report(message)
 843                pytest.fail(message)
 844
 845            logger.write_result_to_html_report(
 846                "NAME Management successfully did not process request with incorrect checksum value"
 847            )
 848
 849
 850class TestActiveDiagnosticTroubleCodes:
 851    """Test that Active Diagnostic Trouble Codes are J1939 Compliant"""
 852
 853    def test_active_diagnostic_trouble_codes(self) -> None:
 854        """
 855        | Description          | Test Active Diagnostic Trouble Codes (DM1)                       |
 856        | :------------------- | :--------------------------------------------------------------- |
 857        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
 858        | Instructions         | 1. Request Active Diagnostic Trouble Codes                  </br>\
 859                                 2. Validate data is within specifications                   </br>\
 860                                 3. Log results                                                   |
 861        | Estimated Duration   | 1 second                                                         |
 862        """
 863        dm1_request = CANFrame(pgn=PGN["Request"], data=[PGN["Active Diagnostic Trouble Codes", [32]].id])
 864        failed_values = []
 865        with CANBus(BATTERY_CHANNEL) as bus:
 866            if dm1_frame := bus.process_call(dm1_request):
 867                if dm1_frame.pgn.id != PGN["Active Diagnostic Trouble Codes", [32]].id:
 868                    Errors.unexpected_packet("Active Diagnostic Trouble Codes", dm1_frame)
 869                    message = f"Unexpected data packet PGN {dm1_frame.pgn.id} was received"
 870                    logger.write_failure_to_html_report(message)
 871                    pytest.fail(message)
 872
 873                logger.write_result_to_html_report(
 874                    "<span style='font-weight: bold'>Active Diagnostic Trouble Codes</span>"
 875                )
 876
 877                dm1_data = dm1_frame.data
 878                pgn_data_field = PGN["Active Diagnostic Trouble Codes"].data_field
 879
 880                for spn, elem in zip(pgn_data_field, dm1_data):
 881                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
 882                    logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
 883                    high_value = 1
 884
 885                    if spn.name in (
 886                        "Protect Lamp",
 887                        "Amber Warning Lamp",
 888                        "Red Stop Lamp",
 889                        "Malfunction Indicator Lamp",
 890                        "DTC1.SPN_Conversion_Method",
 891                    ):
 892                        high_value = 1
 893
 894                    if spn.name in (
 895                        "Flash Protect Lamp",
 896                        "Flash Amber Warning Lamp",
 897                        "Flash Red Stop Lamp",
 898                        "Flash Malfunction Indicator Lamp",
 899                    ):
 900                        high_value = 3
 901
 902                    if spn.name == "DTC1.Suspect_Parameter_Number":
 903                        high_value = 524287
 904
 905                    if spn.name == "DTC1.Failure_Mode_Identifier":
 906                        high_value = 31
 907
 908                    if spn.name == "DTC1.Occurrence_Count":
 909                        high_value = 126
 910
 911                    if not 0 <= elem <= high_value:
 912                        logger.write_warning_to_html_report(
 913                            f"{spn.name}: {cmp(elem, '>=', 0, description)} and "
 914                            f"{cmp(elem, '<=', high_value, description)}"
 915                        )
 916                        failed_values.append(spn.name)
 917
 918                if len(failed_values) > 0:
 919                    categories = "data values" if len(failed_values) > 1 else "data value"
 920                    message = (
 921                        f"{len(failed_values)} {categories} failed "
 922                        f"Active Diagnostic Trouble Code specifications: {', '.join(failed_values)}"
 923                    )
 924                    logger.write_failure_to_html_report(message)
 925                    pytest.fail(message)
 926
 927            else:
 928                message = (
 929                    f"No response was received for mandatory command: "
 930                    f"PGN {PGN['Active Diagnostic Trouble Codes', [32]].id} (Active Diagnostic Trouble Codes)"
 931                )
 932                logger.write_failure_to_html_report(message)
 933                pytest.fail(message)
 934
 935
 936#   TODO(DF): Fix Stop Start Broadcast Test when battery is able to broadcast, test currently blocked
 937# class TestStopStartBroadcast:
 938#     """Test that Stop/Start Broadcast is J1939 Compliant"""
 939#
 940#     @pytest.mark.profile
 941#     def test_profile(self) -> None:
 942#         """
 943#         | Description          | Test that Stop/Start Broadcast (DM13)                            |
 944#         | :------------------- | :--------------------------------------------------------------- |
 945#         | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
 946#         | Instructions         | 1. Create Start Broadcast request                           </br>\
 947#                                  2. Check broadcast began                                    </br>\
 948#                                  3. Create Stop Broadcast request                            </br>\
 949#                                  4. Check broadcast stopped                                  </br>\
 950#                                  5. Create Suspend Broadcast request                         </br>\
 951#                                  6. Check broadcast suspended                                </br>\
 952#                                  7. Log results                                                   |
 953#         | Estimated Duration   | 1 second                                                         |
 954#         """
 955#
 956#         with CANBus(BATTERY_CHANNEL) as bus:
 957#             na_maintenance_mode(bus)
 958#               TODO(DF): Determine order of data (either SAFT or order in j1939da.py)
 959#             broadcast_message_data = {
 960#                 "current_data_link": 1,
 961#                 "sae_j1587": 3,
 962#                 "sae_j1922": 3,
 963#                 "sae_j1939_network_1": 1,
 964#                 "sae_j1939_network_2": 3,
 965#                 "iso_9141": 3,
 966#                 "sae_j1850": 3,
 967#                 "other_manufacturer_specified_port": 3,
 968#                 "sae_j1939_network #3": 3,
 969#                 "proprietary_network_1": 3,
 970#                 "proprietary_network_2": 3,
 971#                 "sae_j1939_network_4": 3,
 972#                 "hold_signal": 15,
 973#                 "suspend_signal": 255,
 974#                 "suspend_duration": 0,
 975#                 "sae_j1939_network_5": 3,
 976#                 "sae_j1939_network_6": 3,
 977#                 "sae_j1939_network_7": 3,
 978#                 "sae_j1939_network_8": 3,
 979#                 "reserved": 2,
 980#                 "sae_j1939_network_9": 3,
 981#                 "sae_j1939_network_10": 3,
 982#                 "sae_j1939_network_11": 3,
 983#             }
 984#             # Order from SAFT Documentation
 985#             # broadcast_message_data = {
 986#             #     "sae_j1939_network_1": 1,
 987#             #     "sae_j1922": 3,
 988#             #     "sae_j1587": 3,
 989#             #     "current_data_link": 1,
 990#             #     "other_manufacturer_specified_port": 3,
 991#             #     "sae_j1850": 3,
 992#             #     "iso_9141": 3,
 993#             #     "sae_j1939_network_2": 3,
 994#             #     "sae_j1939_network_4": 3,
 995#             #     "proprietary_network_2": 3,
 996#             #     "proprietary_network_1": 3,
 997#             #     "sae_j1939_network #3": 3,
 998#             #     "suspend_signal": 255,
 999#             #     "hold_signal": 15,
1000#             #     "suspend_duration": 0,
1001#             #     "sae_j1939_network_8": 3,
1002#             #     "sae_j1939_network_7": 3,
1003#             #     "sae_j1939_network_6": 3,
1004#             #     "sae_j1939_network_5": 3,
1005#             #     "sae_j1939_network_11": 3,
1006#             #     "sae_j1939_network_10": 3,
1007#             #     "sae_j1939_network_9": 3,
1008#             #     "reserved": 2,
1009#             # }
1010#
1011#             dm1_start_request_data = list(broadcast_message_data.values())
1012#
1013#             # Start Broadcast
1014#             dm1_request = CANFrame(pgn=PGN["Stop Start Broadcast"], data=dm1_start_request_data)
1015#             bus.send_message(dm1_request.message())
1016#
1017#             # Check broadcast
1018#             if mystery_response := bus.read_input():
1019#                 logger.write_info_to_report("Broadcast was successfully started")
1020#                 logger.write_info_to_report(f"First broadcast received: {mystery_response}")
1021#             else:
1022#                 message = "Broadcast did not start running Stop Start Broadcast command"
1023#                 logger.write_warning_to_report(message)
1024#                 pytest.fail(message)
1025#
1026#             # Stop Broadcast
1027#             broadcast_message_data["sae_j1939_network_1"] = 3
1028#             broadcast_message_data["current_data_link"] = 3
1029#             broadcast_message_data["hold_signal"] = 1
1030#             dm1_stop_request_data = list(broadcast_message_data.values())
1031#
1032#             dm1_request = CANFrame(pgn=PGN["Stop Start Broadcast"], data=dm1_stop_request_data)
1033#             bus.send_message(dm1_request.message())
1034#
1035#             if mystery_response := bus.read_input():
1036#                 message = "Broadcast did not stop running after Stop Start Broadcast command"
1037#                 logger.write_warning_to_report(message)
1038#                 logger.write_info_to_report(f"Broadcast received: {mystery_response}")
1039#
1040#             else:
1041#                 message = "Broadcast was succesfully stopped after Stop Start Broadcast command"
1042#                 logger.write_info_to_report(message)
1043#
1044#             broadcast_message_data["suspend_signal"] = 0
1045#             dm1_suspend_request_data = list(broadcast_message_data.values())
1046#
1047#             dm1_request = CANFrame(pgn=PGN["Stop Start Broadcast"], data=dm1_suspend_request_data)
1048#             bus.send_message(dm1_request.message())
1049#
1050#             if mystery_response := bus.read_input():
1051#                 message = "Broadcast was not suspended after Stop Start Broadcast command"
1052#                 logger.write_warning_to_report(message)
1053#                 logger.write_warning_to_report(f"Broadcast received: {mystery_response}")
1054#             else:
1055#                 message = "Broadcast was suspended after Stop Start Broadcast command"
1056#                 logger.write_info_to_report(message)
1057#
1058#             logger.write_result_to_html_report("Stop Start Broadcast command was successful")
1059#
1060
1061
1062class TestVehicleElectricalPower:
1063    """Test Vehicle Electrical Power command is J1939 Compliant"""
1064
1065    def test_vehicle_electrical_power(self) -> None:
1066        """
1067        | Description          | Test Vehicle Electrical Power #5 (VEP5) command                  |
1068        | :------------------- | :--------------------------------------------------------------- |
1069        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
1070        | Instructions         | 1. Send Vehicle Electrical Power #5 command                 </br>\
1071                                 2. Validate data is within specifications                   </br>\
1072                                 3. Log response                                                  |
1073        | Estimated Duration   | 1 second                                                         |
1074        """
1075        failed_values = []
1076        with CANBus(BATTERY_CHANNEL) as bus:
1077            vehicle_electrical_power_request = CANFrame(
1078                destination_address=BATTERY_INFO.address,
1079                pgn=PGN["Request"],
1080                data=[PGN["Vehicle Electrical Power #5"].id],
1081            )
1082            if vehicle_electrical_power_response := bus.process_call(vehicle_electrical_power_request):
1083                if vehicle_electrical_power_response.pgn.id != PGN["Vehicle Electrical Power #5", [32]].id:
1084                    if vehicle_electrical_power_response.pgn.id == 59392:
1085                        message = (
1086                            f"Received PGN {vehicle_electrical_power_response.pgn.id} "
1087                            f"({spn_types.acknowledgement(vehicle_electrical_power_response.data[0])}), "
1088                            f'not PGN {PGN["Vehicle Electrical Power #5", [32]].id} '
1089                        )
1090                    else:
1091                        message = (
1092                            f"Received PGN {vehicle_electrical_power_response.pgn.id} "
1093                            f"{vehicle_electrical_power_response.pgn.short_name}, "
1094                            f'not PGN {PGN["Vehicle Electrical Power #5", [32]].id} '
1095                        )
1096                    logger.write_failure_to_html_report(message)
1097                    pytest.fail(message)
1098            else:
1099                message = (
1100                    f"Battery did not respond to PGN {PGN['Vehicle Electrical Power #5', [32]].id} "
1101                    f"Vehicle Electrical Power #5 request"
1102                )
1103                logger.write_failure_to_html_report(message)
1104                pytest.fail(message)
1105
1106            vep5_data = vehicle_electrical_power_response.data
1107            vep5_pgn = PGN["Vehicle Electrical Power #5"].data_field
1108
1109            logger.write_result_to_html_report("<span style='font-weight: bold'>Vehicle Electrical Power #5</span>")
1110
1111            for spn, elem in zip(vep5_pgn, vep5_data):
1112                if spn.name == "Reserved":
1113                    continue
1114
1115                description = f"({desc})" if (desc := spn.data_type(elem)) else ""
1116                logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
1117                high_value = 1.0
1118                if spn.name == "SLI Battery Pack State of Charge":
1119                    high_value = 160.6375
1120
1121                if spn.name == "SLI Battery Pack Capacity":
1122                    high_value = 64255
1123
1124                if spn.name == "SLI Battery Pack Health":
1125                    high_value = 125
1126
1127                if spn.name == "SLI Cranking Predicted Minimum Battery Voltage":
1128                    high_value = 50
1129
1130                if not 0 <= elem <= high_value:
1131                    logger.write_warning_to_html_report(
1132                        f"{spn.name}: {cmp(elem, '>=', 0, description)} and "
1133                        f"{cmp(elem, '<=', high_value, description)}"
1134                    )
1135                    failed_values.append(spn.name)
1136
1137            if len(failed_values) > 0:
1138                categories = "data values" if len(failed_values) > 1 else "data value"
1139                message = (
1140                    f"{len(failed_values)} {categories} failed "
1141                    f"Configuration State Message specifications: {', '.join(failed_values)}"
1142                )
1143                logger.write_failure_to_html_report(message)
1144                pytest.fail(message)
1145
1146            logger.write_result_to_html_report("Test Vehicle Electrical Power #5 command was successful")
1147
1148
1149class TestManufacturerCommands:
1150    """Test Manufacturer Commands are compliant with specs"""
1151
1152    def saft_commands_test(self):
1153        """Tests SAFT Manufacturer Commands"""
1154        with CANBus(BATTERY_CHANNEL) as bus:
1155            manufactured_command_request = CANFrame(destination_address=BATTERY_INFO.address, pgn=PGN["RQST"], data=[0])
1156            invalid_response = []
1157            for address in itertools.chain(
1158                [0xFFD2], range(0xFFD4, 0xFFD9), range(0xFFDC, 0xFFDF), range(0xFFE0, 0xFFE2), [0xFFE4]
1159            ):
1160                manufactured_command_request.data = [address]
1161                default_pgn = PGN[address]
1162                pgn_name = default_pgn.name
1163                logger.write_result_to_html_report(
1164                    f"<span style='font-weight: bold'>PGN {address} ({pgn_name})---</span>"
1165                )
1166
1167                if response_frame := bus.process_call(manufactured_command_request):
1168                    if not response_frame.pgn.id == default_pgn.id:
1169
1170                        if response_frame.pgn.id == PGN["ACKM", [32]].id:
1171                            message = (
1172                                f"Expected {address} ({pgn_name}): Received PGN {response_frame.pgn.id} "
1173                                f"({spn_types.acknowledgement(response_frame.data[0])}) "
1174                            )
1175                            logger.write_warning_to_html_report(message)
1176                        else:
1177                            logger.write_warning_to_html_report(
1178                                f"Expected PGN {address} ({pgn_name}), but received "
1179                                f"{response_frame.pgn.id} ({response_frame.pgn.name}). "
1180                                f"Unable to complete check for command"
1181                            )
1182                        invalid_response.append(f"PGN {address} ({pgn_name})")
1183                        continue
1184                else:
1185                    message = f"Did not receive response from PGN {address} {pgn_name}"
1186                    logger.write_warning_to_html_report(message)
1187                    invalid_response.append(f"PGN {address} ({pgn_name})")
1188
1189                    continue
1190
1191                if response_frame.priority != default_pgn.default_priority:
1192                    message = (
1193                        f"Expected priority level of {default_pgn.default_priority}"
1194                        f" but got priority level {response_frame.priority} for PGN {address}, {pgn_name}"
1195                    )
1196                    invalid_response.append(f"PGN {address} {pgn_name}")
1197                    logger.write_warning_to_html_report(message)
1198
1199                if len(response_frame.packed_data) != 8:
1200                    message = (
1201                        f"Unexpected data length for PGN {address}, {pgn_name}. Expected length of 8, "
1202                        f"received {len(response_frame.packed_data)}"
1203                    )
1204                    logger.write_warning_to_html_report(message)
1205
1206                not_passed_elem = []
1207                for spn, elem in zip(default_pgn.data_field, response_frame.data):
1208                    low_range = 0
1209                    high_range = 3
1210                    spn_name = spn.name
1211                    if spn.name == "Reserved":
1212                        continue
1213
1214                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
1215
1216                    logger.write_result_to_html_report(f"{spn_name}: {elem}{description}")
1217
1218                    if pgn_name == "Battery ECU Status":
1219                        if spn_name in ("Battery Mode", "FET Array State", "SOC Mode", "Heat Reason"):
1220                            high_range = 7
1221                        elif spn_name == "Long-Term Fault Log Status":
1222                            high_range = 15
1223                        elif spn_name == "Software Part Number":
1224                            high_range = 64255
1225
1226                    if pgn_name in ("Battery Cell Status 1", "Battery Cell Status 2"):
1227                        high_range = 6.4255
1228
1229                    if pgn_name == "Battery Performance":
1230                        if spn_name == "Battery Current":
1231                            low_range = -82000.00
1232                            high_range = 82495.35
1233                        if spn_name == "Internal State of Health":
1234                            low_range = -204.800
1235                            high_range = 204.775
1236
1237                    if pgn_name == "Battery Temperatures":
1238                        if spn_name == "MCU Temperature":
1239                            low_range = -40
1240                            high_range = 210
1241                        else:
1242                            low_range = -50
1243                            high_range = 200
1244
1245                    if pgn_name == "Battery Balancing Circuit Info":
1246                        if spn.name == "Cell Voltage Difference":
1247                            high_range = 6.4255
1248                        if spn_name == "Cell Voltage Sum":
1249                            high_range = 104.8576
1250
1251                    if pgn_name in ("Battery Cell Upper SOC", "Battery Cell Lower SOC"):
1252                        low_range = -10
1253                        high_range = 115
1254
1255                    if pgn_name == "Battery Function Status":
1256                        if spn_name == "Heater Set Point":
1257                            low_range = -50
1258                            high_range = 25
1259                        if spn_name == "Storage Delay Time Limit":
1260                            high_range = 65535
1261                        if spn_name == "Last Storage Duration (Minutes)":
1262                            high_range = 59
1263                        if spn_name == "Last Storage Duration (Hours)":
1264                            high_range = 23
1265                        if spn_name == "Last Storage Duration (Days)":
1266                            high_range = 31
1267                        if spn_name == "Last Storage Duration (Months)":
1268                            high_range = 255
1269                        if spn_name == "Effective Reset Time":
1270                            high_range = 60
1271
1272                    if not low_range <= elem <= high_range:
1273                        logger.write_warning_to_html_report(
1274                            f"{spn.name}: {cmp(elem, '>=', 0, description)} and "
1275                            f"{cmp(elem, '<=', high_range, description)}"
1276                        )
1277                        not_passed_elem.append(spn_name)
1278
1279                if len(not_passed_elem) == 0:
1280                    message = f"✅ All data fields in PGN {default_pgn.id} ({pgn_name}) met requirements"
1281                    logger.write_result_to_html_report(message)
1282
1283            if len(invalid_response) > 0:
1284                message = (
1285                    f"{len(invalid_response)} SAFT Manufacturer Command{'s' if len(invalid_response) > 1 else ''} "
1286                    f"failed: {', '.join(invalid_response)}"
1287                )
1288                logger.write_failure_to_html_report(message)
1289                pytest.fail(message)
1290            else:
1291                logger.write_result_to_html_report("All SAFT Manufacturer Commands passed")
1292
1293    def test_manufacturer_commands(self) -> None:
1294        """
1295        | Description          | Fingerprint Manufacturer Commands                                |
1296        | :------------------- | :--------------------------------------------------------------- |
1297        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#388                                 |
1298        | Instructions         | 1. Send request for manufacturer command                    </br>\
1299                                 2. Check values in data                                     </br>\
1300                                 3. Log response                                                  |
1301        | Estimated Duration   | 2 seconds                                                        |
1302        """
1303
1304        if BATTERY_INFO.manufacturer_code == ManufacturerID.SAFT:
1305            self.saft_commands_test()
1306        else:
1307            message = "No known manufacturer commands to test"
1308            logger.write_result_to_html_report(message)
1309            pytest.skip(message)
1310
1311
1312# class TestUDSSession:
1313#     """Tests if batteries respond to UDS Session"""
1314#
1315#     @pytest.mark.profile
1316#     def test_profile(self) -> None:
1317#         """
1318#         | Description          | Test if batteries respond to UDS Session                         |
1319#         | :------------------- | :--------------------------------------------------------------- |
1320#         | GitHub Issue         | turnaroundfactor/BMS-HW-Test#294                                 |
1321#         | Instructions         | 1. Send request for UDS Session                             </br>\
1322#                                  2. Check if battery responds                                </br>\
1323#                                  3. Log response                                                  |
1324#         | Estimated Duration   | 10 minutes                                                       |
1325#         """
1326#
1327#         request_data_params = ["0000", "F18C"]
1328#
1329#         can_256_ids = []
1330#         for number in range(256):
1331#             can_id = 0x18DA00F1 | number << 8
1332#             can_256_ids.append(can_id)
1333#
1334#         read_data_commands = []
1335#         # Read Data by Identifier
1336#         with CANBus(BATTERY_CHANNEL) as bus:
1337#             for address in itertools.chain(
1338#                 [0x7DF], range(0x7E0, 0x7E8), range(0x7E8, 0x7F0), [0x18DB33F1], can_256_ids
1339#             ):
1340#                 respond_command = []
1341#                 for param in request_data_params:
1342#                     data = f"0322{param}AAAAAAAA"
1343#                     bytes_data = bytes.fromhex(data)
1344#                     bus.send_message(can.Message(arbitration_id=address, data=bytes_data))
1345#                     if response := bus.read_input(timeout=0.5):
1346#                         data_frame = CANFrame.decode(response.arbitration_id, response.data)
1347#                         if data_frame.pgn.id != PGN["ACKM"].id:
1348#                             logger.write_info_to_report(
1349#                                 f"UDS Read Memory by Address response received for address {address}: {response}"
1350#                             )
1351#                             respond_command.append(data)
1352#
1353#                 if respond_command:
1354#                     read_data_commands.append(address)
1355#
1356#             if read_data_commands:
1357#                 logger.write_info_to_report(
1358#                     f"The following CAN addresses responded to UDS request: {read_data_commands}"
1359#                 )
1360#             else:
1361#                 logger.write_info_to_report("No UDS responses were received for Read Data By Identifier")
1362#
1363#             # Read Memory Address
1364#             read_memory_commands = []
1365#             for address in itertools.chain(
1366#                 [0x7DF], range(0x7E0, 0x7E8), range(0x7E8, 0x7F0), [0x18DB33F1], can_256_ids
1367#             ):
1368#                 respond_command = []
1369#                 data = "03230000AAAAAAAA"
1370#                 bytes_data = bytes.fromhex(data)
1371#                 bus.send_message(can.Message(arbitration_id=address, data=bytes_data))
1372#                 if response := bus.read_input(timeout=0.5):
1373#                     data_frame = CANFrame.decode(response.arbitration_id, response.data)
1374#                     if data_frame.pgn.id != PGN["ACKM"].id:
1375#                         logger.write_info_to_report(
1376#                             f"UDS Read Memory by Address response received for address {address}: {response}"
1377#                         )
1378#                         respond_command.append(data)
1379#
1380#                 if respond_command:
1381#                     read_memory_commands.append(address)
1382#
1383#             if read_memory_commands:
1384#                 logger.write_info_to_report(
1385#                     f"The following CAN addresses responded to UDS request: {read_memory_commands}"
1386#                 )
1387#             else:
1388#                 logger.write_info_to_report("No UDS responses were received for Read Memory By Address")
1389#
1390#             _GENERATED_PROFILE.uds_read_data_commands = read_data_commands
1391#             _GENERATED_PROFILE.uds_read_memory_commands = read_memory_commands
1392#
1393#     def test_comparison(self) -> None:
1394#         """
1395#         | Description          | Compare UDS responses                                            |
1396#         | :------------------- | :--------------------------------------------------------------- |
1397#         | GitHub Issue         | turnaroundfactor/BMS-HW-Test#294                                 |
1398#         | Instructions         | 1. Check if profiles have similar UDS responses                  |
1399#         | Pass / Fail Criteria | Pass if profiles are closely matched                             |
1400#         | Estimated Duration   | 1 second                                                         |
1401#         """
1402#
1403#         if len(_GENERATED_PROFILE.uds_read_data_commands) == 0 and
1404#         len(_COMPARISON_PROFILE.uds_read_data_commands) == 0:
1405#             logger.write_result_to_report("Both profiles did not receive UDS responses")
1406#
1407#         if (
1408#             len(_GENERATED_PROFILE.uds_read_memory_commands) == 0
1409#             and len(_COMPARISON_PROFILE.uds_read_memory_commands) == 0
1410#         ):
1411#             logger.write_result_to_report("Both profiles did not receive UDS responses")
1412#
1413#         unique_read_data_commands = set(_GENERATED_PROFILE.uds_read_data_commands) - set(
1414#             _COMPARISON_PROFILE.uds_read_data_commands
1415#         )
1416#         unique_read_memory_commands = set(_GENERATED_PROFILE.uds_read_memory_commands) - set(
1417#             _COMPARISON_PROFILE.uds_read_memory_commands
1418#         )
1419#
1420#         if unique_read_data_commands:
1421#             logger.write_warning_to_report(
1422#                 f"Profiles did not match read data CAN addresses: {unique_read_data_commands}"
1423#             )
1424#
1425#         if unique_read_memory_commands:
1426#             logger.write_warning_to_report(
1427#                 f"Profiles did not match read memory CAN addresses: {unique_read_memory_commands}"
1428#             )
1429#
1430#         if not unique_read_data_commands and not unique_read_memory_commands:
1431#             logger.write_result_to_html_report("Profiles shared similar results for UDS requests")
1432#
1433
1434
1435class TestECUInformation:
1436    """Gets information about the physical ECU and its hardware"""
1437
1438    @staticmethod
1439    def bytes_to_ascii(bs: list[float]) -> list[str]:
1440        """Converts bytes to ASCII string"""
1441        s: str = ""
1442        for b in bs:
1443            h = re.sub(r"^[^0-9a-fA-F]+$", "", f"{b:x}")
1444            try:
1445                ba = bytearray.fromhex(h)[::-1]
1446                s += ba.decode("utf-8", "ignore")
1447                s = re.sub(r"[^\x20-\x7E]", "", s)
1448            except ValueError:
1449                # NOTE: This will ignore any invalid packets (from BrenTronics)
1450                logger.write_warning_to_report(f"Skipping invalid hex: {b:x}")
1451        return list(filter(None, s.split("*")))
1452
1453    def test_ecu_information(self) -> None:
1454        """
1455        | Description          | Get information from ECUID response                              |
1456        | :------------------- | :--------------------------------------------------------------- |
1457        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#395                                 |
1458        | Instructions         | 1. Request ECUID data                                       </br>\
1459                                 2. Validate data is within specifications                   </br>\
1460                                 3. Log response                                                  |
1461        | Estimated Duration   | 10 seconds                                                       |
1462        """
1463
1464        info = []
1465        with CANBus(BATTERY_CHANNEL) as bus:
1466            ecu_request = CANFrame(pgn=PGN["Request"], data=[PGN["ECUID"].id])
1467            if tp_cm_frame := bus.process_call(ecu_request):
1468                if tp_cm_frame is not None:
1469                    if tp_cm_frame.pgn.id == PGN["TP.CM", [32]].id:
1470                        data = []
1471                        cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0])
1472                        if data_frame := bus.process_call(cts_request):
1473                            if data_frame is not None:
1474                                if data_frame.pgn.id == PGN["TP.DT"].id:
1475                                    packet_count = int(tp_cm_frame.data[2])
1476                                    # NOTE: This will ignore the invalid header packet from BrenTronics
1477                                    if BATTERY_INFO.manufacturer_code != ManufacturerID.BRENTRONICS:
1478                                        data.append(data_frame.data[1])
1479                                        packet_count -= 1
1480                                    for _ in range(packet_count):
1481                                        data_message = bus.read_input()
1482                                        if data_message is not None:
1483                                            frame = CANFrame.decode(data_message.arbitration_id, data_message.data)
1484                                            if frame.pgn.id == PGN["TP.DT"].id:
1485                                                data.append(frame.data[1])
1486                                            else:
1487                                                Errors.unexpected_packet("TP.DT", frame)
1488                                                break
1489                                        else:
1490                                            Errors.no_packet("TP.DT")
1491                                            break
1492                                    info = self.bytes_to_ascii(data)
1493                                    eom_request = CANFrame(
1494                                        pgn=PGN["TP.CM"],
1495                                        data=[17, tp_cm_frame.data[1], packet_count, 0xFF, tp_cm_frame.data[-1]],
1496                                    )
1497                                    if eom_frame := bus.process_call(eom_request):
1498                                        if eom_frame is not None:
1499                                            if eom_frame.pgn.id == PGN["DM15"].id:
1500                                                if eom_frame.data[2] == 4:  # Operation Completed
1501                                                    logger.write_info_to_report("ECUID data transfer successful")
1502                                                else:
1503                                                    logger.write_warning_to_html_report("Unsuccessful EOM response")
1504                                            else:
1505                                                Errors.unexpected_packet("DM15", eom_frame)
1506                                        else:
1507                                            Errors.no_packet("DM15")
1508                                    else:
1509                                        # timeout
1510                                        logger.write_warning_to_report("No response after sending EOM (DM15)")
1511                                else:
1512                                    Errors.unexpected_packet("TP.DT", data_frame)
1513                            else:
1514                                Errors.no_packet("TP.DT")
1515                        else:
1516                            message = f"Did not receive response from PGN {PGN['TP.CM', [32]].id} (TP.CM)"
1517                            logger.write_failure_to_html_report(message)
1518                            pytest.fail(message)
1519                    else:
1520                        Errors.unexpected_packet("TP.CM", tp_cm_frame)
1521                else:
1522                    Errors.no_packet("TP.CM")
1523            else:
1524                message = f"Did not receive response from PGN {PGN['ECUID', [32]].id}"
1525                logger.write_failure_to_html_report(message)
1526                pytest.fail(message)
1527
1528            if len(info) > 0:
1529                ecu = {
1530                    "part_number": info[0],
1531                    "serial_number": info[1],
1532                    "location_name": info[2],
1533                    "manufacturer": info[3],
1534                    "classification": info[4],
1535                }
1536                logger.write_result_to_html_report("<span style='font-weight: bold'>ECUID Information </span>")
1537                for key, value in ecu.items():
1538                    logger.write_result_to_html_report(f"{key.strip().replace('_', ' ').title()}: {value}")
1539            else:
1540                logger.write_failure_to_html_report("Could not get ECU information")
1541                pytest.fail("Could not get ECU information")
1542
1543
1544def memory_test(mode: Modes) -> list[int]:
1545    """Memory tester helper"""
1546    found_addresses = []
1547    with CANBus(BATTERY_CHANNEL) as bus:
1548        read_request_frame = CANFrame(pgn=PGN["DM14"], data=[1, 1, 1, 0, 0, 0, 0, 0])
1549        read_request_frame.data[2] = mode
1550        addresses = [0, 1107296256, 2147483648, 4294966271]  # 0x0, 0x42000000, 0x80000000, 0xfffffbff
1551        for low_address in addresses:
1552            found = 0
1553            high_address = low_address + (1024 if FULL_MEMORY_TESTS else 16)
1554            for i in range(low_address, high_address):
1555                read_request_frame.data[5] = low_address
1556                if response_frame := bus.process_call(read_request_frame, timeout=1):  # NOTE: SAFT times out
1557                    if response_frame is not None:
1558                        logger.write_info_to_report(
1559                            f"Address {i} responded with PGN {response_frame.pgn.id} "
1560                            f"({response_frame.pgn.short_name}) - status is: "
1561                            f"{spn_types.dm15_status(response_frame.data[3])}"
1562                        )
1563                        if response_frame.pgn.id == PGN["DM15"].id:
1564                            if response_frame.data[5] != 258:  # Invalid Length
1565                                if response_frame.data[3] == 0:
1566                                    found_addresses.append(i)
1567                                    found += 1
1568                        else:
1569                            Errors.unexpected_packet("DM15", response_frame)
1570                            break
1571                    else:
1572                        Errors.no_packet("DM15")
1573                        break
1574                else:
1575                    # timeout
1576                    pass
1577            verb = ""
1578            match mode:
1579                case Modes.READ:
1580                    verb = "readable"
1581                case Modes.WRITE:
1582                    verb = "writable"
1583                case Modes.ERASE:
1584                    verb = "erasable"
1585                case Modes.BOOT:
1586                    verb = "boot load"
1587            message = (
1588                f"Found {found} {verb} successful address(es) in memory ranges"
1589                f" {hex(low_address)}-{hex(high_address)}"
1590            )
1591            logger.write_result_to_html_report(message)
1592
1593    if len(found_addresses) > 0:
1594        logger.write_result_to_html_report(
1595            f"Found {len(found_addresses)} {verb} memory address(es) out of "
1596            f"{4096 if FULL_MEMORY_TESTS else 64} possible addresses"
1597        )
1598    else:
1599        message = f"Found 0 {verb} memory addresses out of {4096 if FULL_MEMORY_TESTS else 64} possible addresses"
1600        logger.write_warning_to_html_report(message)
1601    return found_addresses
1602
1603
1604class TestMemoryRead:
1605    """This will test the read capability of the memory"""
1606
1607    def test_read(self) -> None:
1608        """
1609        | Description          | Try to read from different memory locations                      |
1610        | :------------------- | :--------------------------------------------------------------- |
1611        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#379                            </br>\
1612                                 turnaroundfactor/BMS-HW-Test#380                                 |
1613        | Instructions         | 1. Request memory read                                      </br>\
1614                                 2. Log any successful addresses                                  |
1615        | Estimated Duration   | 1 minute                                                         |
1616        """
1617        memory_test(Modes.READ)
1618
1619
1620class TestMemoryWrite:
1621    """This will test the write capability of the memory"""
1622
1623    def test_write(self) -> None:
1624        """
1625        | Description          | Try to write to different memory locations                       |
1626        | :------------------- | :--------------------------------------------------------------- |
1627        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#379                            </br>\
1628                                 turnaroundfactor/BMS-HW-Test#380                                 |
1629        | Instructions         | 1. Request memory write                                     </br>\
1630                                 2. Log any successful addresses                                  |
1631        | Estimated Duration   | 1 minute                                                         |
1632        """
1633        memory_test(Modes.WRITE)
1634
1635
1636class TestMemoryErase:
1637    """This will test the erase capability of the memory"""
1638
1639    def test_erase(self) -> None:
1640        """
1641        | Description          | Try to erase different memory locations                          |
1642        | :------------------- | :--------------------------------------------------------------- |
1643        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#379                            </br>\
1644                                 turnaroundfactor/BMS-HW-Test#380                                 |
1645        | Instructions         | 1. Request memory erase                                     </br>\
1646                                 2. Log any successful addresses                                  |
1647        | Estimated Duration   | 1 minute                                                         |
1648        """
1649        memory_test(Modes.ERASE)
1650
1651
1652class TestMemoryBootLoad:
1653    """This will test the boot load capability of the memory"""
1654
1655    def test_boot(self) -> None:
1656        """
1657        | Description          | Try to boot load from different memory locations                 |
1658        | :------------------- | :--------------------------------------------------------------- |
1659        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#381                            </br>\
1660                                 turnaroundfactor/BMS-HW-Test#382                                 |
1661        | Instructions         | 1. Request memory boot load                                 </br>\
1662                                 2. Log any successful addresses                                  |
1663        | Estimated Duration   | 1 minute                                                         |
1664        """
1665        memory_test(Modes.BOOT)
BATTERY_CHANNEL = 'can0'

Channel identification. Expected type is backend dependent.

HARD_RESET = False

Whether to use a soft or hard reset before each test.

PROFILE = ''

The name of the profile to compare against. If this is not provided, a profile json will be generated.

FULL_MEMORY_TESTS = False

Short or long memory tests

ADDRESS = 232
BATTERY_INFO = namespace()
class ManufacturerID(enum.IntEnum):
70class ManufacturerID(IntEnum):
71    """Enum to hold Manufacturer IDs."""
72
73    SAFT = 269  # NOTE: unused
74    BRENTRONICS = 822

Enum to hold Manufacturer IDs.

SAFT = <ManufacturerID.SAFT: 269>
BRENTRONICS = <ManufacturerID.BRENTRONICS: 822>
Inherited Members
enum.Enum
name
value
builtins.int
conjugate
bit_length
bit_count
to_bytes
from_bytes
as_integer_ratio
is_integer
real
imag
numerator
denominator
class Modes(builtins.float, enum.Enum):
77class Modes(float, Enum):
78    """Enum for Command modes"""
79
80    ERASE = 0
81    READ = 1
82    WRITE = 2
83    BOOT = 6
84    EDCP = 7

Enum for Command modes

ERASE = <Modes.ERASE: 0.0>
READ = <Modes.READ: 1.0>
WRITE = <Modes.WRITE: 2.0>
BOOT = <Modes.BOOT: 6.0>
EDCP = <Modes.EDCP: 7.0>
Inherited Members
enum.Enum
name
value
builtins.float
conjugate
as_integer_ratio
fromhex
hex
is_integer
real
imag
class BadStates(enum.Enum):
87class BadStates(Enum):
88    """Enum for Bad Test Response States"""
89
90    NO_RESPONSE = 0
91    WRONG_PGN = 1
92    NACK = 2
93    WRONG_PACKETS = 3
94    INVALID_RESPONSE = 4

Enum for Bad Test Response States

NO_RESPONSE = <BadStates.NO_RESPONSE: 0>
WRONG_PGN = <BadStates.WRONG_PGN: 1>
NACK = <BadStates.NACK: 2>
WRONG_PACKETS = <BadStates.WRONG_PACKETS: 3>
INVALID_RESPONSE = <BadStates.INVALID_RESPONSE: 4>
Inherited Members
enum.Enum
name
value
class Errors:
 97class Errors:
 98    """Holds different error messages"""
 99
100    @staticmethod
101    def timeout() -> None:
102        """Prints message when connection times out"""
103        message = "Could not locate 6T"
104        logger.write_critical_to_report(message)
105        pytest.exit(message)
106
107    @staticmethod
108    def unexpected_packet(expected: str, frame: CANFrame) -> None:
109        """Prints message when unexpected packet is received"""
110        logger.write_warning_to_report(f"Expected {expected}, got {frame.pgn.short_name}")
111
112    @staticmethod
113    def no_packet(expected: str) -> None:
114        """Prints message when no packet is received"""
115        logger.write_warning_to_report(f"Expected {expected}, got None")

Holds different error messages

@staticmethod
def timeout() -> None:
100    @staticmethod
101    def timeout() -> None:
102        """Prints message when connection times out"""
103        message = "Could not locate 6T"
104        logger.write_critical_to_report(message)
105        pytest.exit(message)

Prints message when connection times out

@staticmethod
def unexpected_packet( expected: str, frame: hitl_tester.modules.cyber_6t.canbus.CANFrame) -> None:
107    @staticmethod
108    def unexpected_packet(expected: str, frame: CANFrame) -> None:
109        """Prints message when unexpected packet is received"""
110        logger.write_warning_to_report(f"Expected {expected}, got {frame.pgn.short_name}")

Prints message when unexpected packet is received

@staticmethod
def no_packet(expected: str) -> None:
112    @staticmethod
113    def no_packet(expected: str) -> None:
114        """Prints message when no packet is received"""
115        logger.write_warning_to_report(f"Expected {expected}, got None")

Prints message when no packet is received

def cmp( a: float | str, sign: Literal['<', '<=', '>', '>=', '=='], b: float | str, unit_a: str = '', unit_b: str = '', form: str = '') -> str:
121def cmp(
122    a: float | str,
123    sign: Literal["<", "<=", ">", ">=", "=="],
124    b: float | str,
125    unit_a: str = "",
126    unit_b: str = "",
127    form: str = "",
128) -> str:
129    """Generate a formatted string based on a comparison."""
130    if not unit_b:
131        unit_b = unit_a
132
133    if isinstance(a, str) or isinstance(b, str):
134        return f"{a:{form}}{unit_a} {('≠', '=')[a == b]} {b:{form}}{unit_b}"
135
136    sign_str = {
137        "<": ("≮", "<")[a < b],
138        "<=": ("≰", "≤")[a <= b],
139        ">": ("≯", ">")[a > b],
140        ">=": ("≱", "≥")[a >= b],
141        "==": ("≠", "=")[a == b],
142    }
143
144    return f"{a:{form}}{unit_a} {sign_str[sign]} {b:{form}}{unit_b}"

Generate a formatted string based on a comparison.

@pytest.fixture(scope='class', autouse=True)
def reset_test_environment():
147@pytest.fixture(scope="class", autouse=True)
148def reset_test_environment():
149    """Before each test class, reset the 6T."""
150
151    try:
152        power_cycle_frame = CANFrame(
153            destination_address=BATTERY_INFO.address,
154            pgn=PGN["PropA"],
155            data=[0, 0, 1, 1, 1, not HARD_RESET, 1, 3, 0, -1],
156        )
157        maintenance_mode_frame = CANFrame(
158            destination_address=BATTERY_INFO.address,
159            pgn=PGN["PropA"],
160            data=[0, 0, 1, 1, 1, 3, 1, 3, 0, -1],
161        )
162        factory_reset_frame = CANFrame(
163            destination_address=BATTERY_INFO.address,
164            pgn=PGN["PropA"],
165            data=[1, 0, 0, -1, 3, 0xF, 3, 0, 0x1F, -1],
166        )
167        with CANBus(BATTERY_CHANNEL) as bus:
168            logger.write_info_to_report("Power-Cycling 6T")
169            bus.process_call(power_cycle_frame)
170            time.sleep(10)
171            logger.write_info_to_report("Entering maintenance mode")
172            bus.process_call(maintenance_mode_frame)
173            logger.write_info_to_report("Factory resetting 6T")
174            bus.process_call(factory_reset_frame)
175            time.sleep(10)
176    except AttributeError:  # Battery has not yet been found
177        return

Before each test class, reset the 6T.

class TestLocate6T:
180class TestLocate6T:
181    """Scan for 6T battery."""
182
183    def test_locate_6t(self) -> None:
184        """
185        | Description          | Scan the bus for devices                                         |
186        | :------------------- | :--------------------------------------------------------------- |
187        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#396                                 |
188        | Instructions         | 1. Request the names of everyone on the bus                 </br>\
189                                 2. Use the response data for all communication                   |
190        | Estimated Duration   | 1 second                                                         |
191        """
192        name_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id])
193        with CANBus(BATTERY_CHANNEL) as bus:
194            if name_frame := bus.process_call(name_request):
195                # Save responses to profile
196                BATTERY_INFO.id = int(name_frame.data[0])
197                BATTERY_INFO.manufacturer_code = name_frame.data[1]
198                if (mfg_name := spn_types.manufacturer_id(name_frame.data[1])) == "Reserved":
199                    mfg_name = f"Unknown ({name_frame.data[1]})"
200                BATTERY_INFO.manufacturer_name = mfg_name
201                BATTERY_INFO.address = name_frame.source_address
202            else:
203                message = "Could not locate 6T"
204                logger.write_warning_to_html_report(message)
205                pytest.exit(message)
206
207        # Log results
208        printable_id = "".join(
209            chr(i) if chr(i).isprintable() else "." for i in BATTERY_INFO.id.to_bytes(3, byteorder="big")
210        )
211        logger.write_result_to_html_report(
212            f"Found {BATTERY_INFO.manufacturer_name} 6T at address {BATTERY_INFO.address} "
213            f"(ID: {BATTERY_INFO.id:06X}, ASCII ID: {printable_id})"
214        )

Scan for 6T battery.

def test_locate_6t(self) -> None:
183    def test_locate_6t(self) -> None:
184        """
185        | Description          | Scan the bus for devices                                         |
186        | :------------------- | :--------------------------------------------------------------- |
187        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#396                                 |
188        | Instructions         | 1. Request the names of everyone on the bus                 </br>\
189                                 2. Use the response data for all communication                   |
190        | Estimated Duration   | 1 second                                                         |
191        """
192        name_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id])
193        with CANBus(BATTERY_CHANNEL) as bus:
194            if name_frame := bus.process_call(name_request):
195                # Save responses to profile
196                BATTERY_INFO.id = int(name_frame.data[0])
197                BATTERY_INFO.manufacturer_code = name_frame.data[1]
198                if (mfg_name := spn_types.manufacturer_id(name_frame.data[1])) == "Reserved":
199                    mfg_name = f"Unknown ({name_frame.data[1]})"
200                BATTERY_INFO.manufacturer_name = mfg_name
201                BATTERY_INFO.address = name_frame.source_address
202            else:
203                message = "Could not locate 6T"
204                logger.write_warning_to_html_report(message)
205                pytest.exit(message)
206
207        # Log results
208        printable_id = "".join(
209            chr(i) if chr(i).isprintable() else "." for i in BATTERY_INFO.id.to_bytes(3, byteorder="big")
210        )
211        logger.write_result_to_html_report(
212            f"Found {BATTERY_INFO.manufacturer_name} 6T at address {BATTERY_INFO.address} "
213            f"(ID: {BATTERY_INFO.id:06X}, ASCII ID: {printable_id})"
214        )
Description Scan the bus for devices
GitHub Issue turnaroundfactor/BMS-HW-Test#396
Instructions 1. Request the names of everyone on the bus
2. Use the response data for all communication
Estimated Duration 1 second
class TestNameInformation:
217class TestNameInformation:
218    """Check Unique ID (Identity Number) field in Address Claimed message"""
219
220    def test_name_information(self) -> None:
221        """
222        | Description          | Confirm information in NAME (address claimed) matches            \
223                                 expected value.                                                  |
224        | :------------------- | :--------------------------------------------------------------- |
225        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#396                                 |
226        | Instructions         | 1. Request Address Claimed data                             </br>\
227                                 2. Log returned values                                      </br>\
228                                 3. Validate if values meet spec requirements                     |
229        | Estimated Duration   | 1 second                                                         |
230        """
231
232        name_values = {}
233        failed_values = []
234
235        name_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id])
236        with CANBus(BATTERY_CHANNEL) as bus:
237            if name_frame := bus.process_call(name_request):
238                name_values["identity_number"] = int(name_frame.data[0])
239                vehicle_system_instance = 0
240
241                for spn, elem in zip(PGN["Address Claimed"].data_field, name_frame.data):
242                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
243                    logger.write_info_to_report(f"{spn.name}: {elem} {description}")
244
245                    if spn.name in ("Manufacturer Code", "Reserved"):
246                        continue
247
248                    high_range = 0
249                    if spn.name == "Identity Number":
250                        high_range = 2097151
251
252                    if spn.name == "ECU Instance":
253                        high_range = 7
254
255                    if spn.name == "Function Instance":
256                        high_range = 31
257
258                    if spn.name == "Function":
259                        high_range = 254
260
261                    if spn.name == "Vehicle System Instance":
262                        high_range = 15
263                        vehicle_system_instance = elem
264
265                    if spn.name == "Industry Group":
266                        high_range = 7
267
268                    if spn.name == "Arbitrary Address Capable":
269                        high_range = 1
270
271                    if spn.name == "Vehicle System":
272                        if elem not in (127, 0, vehicle_system_instance):
273                            logger.write_warning_to_html_report(
274                                f"Vehicle System {elem} is not 127, 0, or the same value as " f"Vehicle System Instance"
275                            )
276                            failed_values.append(spn.name)
277                    else:
278                        if not 0 <= elem <= high_range:
279                            logger.write_warning_to_html_report(
280                                f"{spn.name}: {cmp(elem, '>=', 0)} and " f"{cmp(elem, '<=', high_range)}"
281                            )
282                            failed_values.append(spn.name)
283
284            else:
285                message = f"No response to PGN {PGN['Address Claimed', [32]].id} (Address Claimed)"
286                logger.write_failure_to_html_report(message)
287                pytest.fail(message)
288
289        if len(failed_values) > 0:
290            categories = "data values" if len(failed_values) > 1 else "data value"
291            message = (
292                f"{len(failed_values)} {categories} failed Address Claimed specifications: {', '.join(failed_values)}"
293            )
294            logger.write_failure_to_html_report(message)
295            pytest.fail(message)
296
297        logger.write_result_to_html_report(
298            f"Found Identity Number: {name_values['identity_number']} (0x{name_values['identity_number']:06x}) "
299            f"for {BATTERY_INFO.manufacturer_name} 6T at address {BATTERY_INFO.address}"
300        )

Check Unique ID (Identity Number) field in Address Claimed message

def test_name_information(self) -> None:
220    def test_name_information(self) -> None:
221        """
222        | Description          | Confirm information in NAME (address claimed) matches            \
223                                 expected value.                                                  |
224        | :------------------- | :--------------------------------------------------------------- |
225        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#396                                 |
226        | Instructions         | 1. Request Address Claimed data                             </br>\
227                                 2. Log returned values                                      </br>\
228                                 3. Validate if values meet spec requirements                     |
229        | Estimated Duration   | 1 second                                                         |
230        """
231
232        name_values = {}
233        failed_values = []
234
235        name_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id])
236        with CANBus(BATTERY_CHANNEL) as bus:
237            if name_frame := bus.process_call(name_request):
238                name_values["identity_number"] = int(name_frame.data[0])
239                vehicle_system_instance = 0
240
241                for spn, elem in zip(PGN["Address Claimed"].data_field, name_frame.data):
242                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
243                    logger.write_info_to_report(f"{spn.name}: {elem} {description}")
244
245                    if spn.name in ("Manufacturer Code", "Reserved"):
246                        continue
247
248                    high_range = 0
249                    if spn.name == "Identity Number":
250                        high_range = 2097151
251
252                    if spn.name == "ECU Instance":
253                        high_range = 7
254
255                    if spn.name == "Function Instance":
256                        high_range = 31
257
258                    if spn.name == "Function":
259                        high_range = 254
260
261                    if spn.name == "Vehicle System Instance":
262                        high_range = 15
263                        vehicle_system_instance = elem
264
265                    if spn.name == "Industry Group":
266                        high_range = 7
267
268                    if spn.name == "Arbitrary Address Capable":
269                        high_range = 1
270
271                    if spn.name == "Vehicle System":
272                        if elem not in (127, 0, vehicle_system_instance):
273                            logger.write_warning_to_html_report(
274                                f"Vehicle System {elem} is not 127, 0, or the same value as " f"Vehicle System Instance"
275                            )
276                            failed_values.append(spn.name)
277                    else:
278                        if not 0 <= elem <= high_range:
279                            logger.write_warning_to_html_report(
280                                f"{spn.name}: {cmp(elem, '>=', 0)} and " f"{cmp(elem, '<=', high_range)}"
281                            )
282                            failed_values.append(spn.name)
283
284            else:
285                message = f"No response to PGN {PGN['Address Claimed', [32]].id} (Address Claimed)"
286                logger.write_failure_to_html_report(message)
287                pytest.fail(message)
288
289        if len(failed_values) > 0:
290            categories = "data values" if len(failed_values) > 1 else "data value"
291            message = (
292                f"{len(failed_values)} {categories} failed Address Claimed specifications: {', '.join(failed_values)}"
293            )
294            logger.write_failure_to_html_report(message)
295            pytest.fail(message)
296
297        logger.write_result_to_html_report(
298            f"Found Identity Number: {name_values['identity_number']} (0x{name_values['identity_number']:06x}) "
299            f"for {BATTERY_INFO.manufacturer_name} 6T at address {BATTERY_INFO.address}"
300        )
Description Confirm information in NAME (address claimed) matches expected value.
GitHub Issue turnaroundfactor/BMS-HW-Test#396
Instructions 1. Request Address Claimed data
2. Log returned values
3. Validate if values meet spec requirements
Estimated Duration 1 second
class TestSoftwareIdentificationInformation:
303class TestSoftwareIdentificationInformation:
304    """Retrieve & Validate Software Identification Information"""
305
306    def test_software_identification_information(self) -> None:
307        """
308        | Description          | Validate Software Identification                                 |
309        | :------------------- | :--------------------------------------------------------------- |
310        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#394                                 |
311        | Instructions         | 1. Request Software Identification                          </br>\
312                                 2. Validate Software Identification                         </br>\
313                                 3. Log Response                                                  |
314        | Estimated Duration   | 1 second                                                         |
315        """
316
317        soft_request = CANFrame(pgn=PGN["RQST"], data=[0xFEDA])
318        soft_data = []
319        with CANBus(BATTERY_CHANNEL) as bus:
320            if soft_frame := bus.process_call(soft_request):
321                # Complete RTS
322                if len(soft_frame.data) < 3:
323                    message = (
324                        f"Unexpected byte response from PGN "
325                        f"{PGN['Software Identification', [32]].id} (Software Identification)"
326                    )
327                    logger.write_warning_to_html_report(message)
328                    return
329                expected_bytes = int(soft_frame.data[1])
330                expected_packets = 0
331                if BATTERY_INFO.manufacturer_code == ManufacturerID.BRENTRONICS:
332                    expected_packets = int(soft_frame.data[2] + 1)
333                else:
334                    expected_packets = int(soft_frame.data[2])
335
336                rts_pgn_id = int(soft_frame.data[-1])
337                cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0])
338                cts_request.data[1] = expected_packets
339                cts_request.data[-1] = rts_pgn_id
340
341                # Send CTS
342                bus.send_message(cts_request.message())
343
344                # Read & Store data from packets
345                for i in range(expected_packets):
346                    data_frame = bus.read_frame()
347                    if data_frame.pgn.id != 0xEB00:
348                        message = f"Expected data frame {i + 1}, got PGN {data_frame.pgn.id}"
349                        logger.write_failure_to_html_report(message)
350                        pytest.fail(message)
351                    soft_data.append(hex(int(data_frame.data[1])))
352
353                if len(soft_data) != expected_packets:
354                    message = f"Expected {expected_packets} packets, got {len(soft_data)}"
355                    logger.write_failure_to_html_report(message)
356                    pytest.fail(message)
357
358                # Send acknowledgement frame
359                end_acknowledge_frame = CANFrame(pgn=PGN["TP.CM"], data=[19, 0, 0, 0xFF, 0])
360                end_acknowledge_frame.data[1] = expected_bytes
361                end_acknowledge_frame.data[2] = expected_packets
362                end_acknowledge_frame.data[-1] = rts_pgn_id
363
364                bus.send_message(end_acknowledge_frame.message())
365            else:
366                message = f"No response for PGN {PGN['Software Identification', [32]].id} (Software Identification)"
367                logger.write_warning_to_html_report(message)
368                return
369
370        hex_string = "0x"
371        add_to_data = False
372        finish_adding = False
373
374        # Find Software Identification Number
375        for element in soft_data:
376            n = len(element) - 1
377            while n >= 1:
378                if add_to_data:
379                    if element[n - 1 : n + 1] == "2a":
380                        finish_adding = True
381                        break
382                    if element[n - 1 : n + 1] != "0x":
383                        hex_string += element[n - 1 : n + 1]
384                        n -= 2
385                    else:
386                        n -= 2
387                else:
388                    if element[n - 3 : n + 1] == "2a31" or element[n - 3 : n + 1] == "2a01":
389                        add_to_data = True
390                        n -= 4
391                    else:
392                        n -= 4
393            if finish_adding:
394                break
395
396        if len(hex_string) <= 2:
397            logger.write_warning_to_html_report(
398                f"Software Identification: {hex_string} was not found in expected format"
399            )
400            return
401
402        software_identification = spn_types.ascii_map(int(hex_string, 16))
403
404        if re.search(r"[0-9]{2}\.[0-9]{2}\.[a-z]{2}\.[a-z]{2,}", software_identification) is None:
405            message = f"Software Identification: {software_identification} was not in expected format: MM.II.mm.aa.ee"
406            logger.write_failure_to_html_report(message)
407            pytest.fail(message)
408
409        logger.write_result_to_html_report(f"Software Identification: {software_identification}")

Retrieve & Validate Software Identification Information

def test_software_identification_information(self) -> None:
306    def test_software_identification_information(self) -> None:
307        """
308        | Description          | Validate Software Identification                                 |
309        | :------------------- | :--------------------------------------------------------------- |
310        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#394                                 |
311        | Instructions         | 1. Request Software Identification                          </br>\
312                                 2. Validate Software Identification                         </br>\
313                                 3. Log Response                                                  |
314        | Estimated Duration   | 1 second                                                         |
315        """
316
317        soft_request = CANFrame(pgn=PGN["RQST"], data=[0xFEDA])
318        soft_data = []
319        with CANBus(BATTERY_CHANNEL) as bus:
320            if soft_frame := bus.process_call(soft_request):
321                # Complete RTS
322                if len(soft_frame.data) < 3:
323                    message = (
324                        f"Unexpected byte response from PGN "
325                        f"{PGN['Software Identification', [32]].id} (Software Identification)"
326                    )
327                    logger.write_warning_to_html_report(message)
328                    return
329                expected_bytes = int(soft_frame.data[1])
330                expected_packets = 0
331                if BATTERY_INFO.manufacturer_code == ManufacturerID.BRENTRONICS:
332                    expected_packets = int(soft_frame.data[2] + 1)
333                else:
334                    expected_packets = int(soft_frame.data[2])
335
336                rts_pgn_id = int(soft_frame.data[-1])
337                cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0])
338                cts_request.data[1] = expected_packets
339                cts_request.data[-1] = rts_pgn_id
340
341                # Send CTS
342                bus.send_message(cts_request.message())
343
344                # Read & Store data from packets
345                for i in range(expected_packets):
346                    data_frame = bus.read_frame()
347                    if data_frame.pgn.id != 0xEB00:
348                        message = f"Expected data frame {i + 1}, got PGN {data_frame.pgn.id}"
349                        logger.write_failure_to_html_report(message)
350                        pytest.fail(message)
351                    soft_data.append(hex(int(data_frame.data[1])))
352
353                if len(soft_data) != expected_packets:
354                    message = f"Expected {expected_packets} packets, got {len(soft_data)}"
355                    logger.write_failure_to_html_report(message)
356                    pytest.fail(message)
357
358                # Send acknowledgement frame
359                end_acknowledge_frame = CANFrame(pgn=PGN["TP.CM"], data=[19, 0, 0, 0xFF, 0])
360                end_acknowledge_frame.data[1] = expected_bytes
361                end_acknowledge_frame.data[2] = expected_packets
362                end_acknowledge_frame.data[-1] = rts_pgn_id
363
364                bus.send_message(end_acknowledge_frame.message())
365            else:
366                message = f"No response for PGN {PGN['Software Identification', [32]].id} (Software Identification)"
367                logger.write_warning_to_html_report(message)
368                return
369
370        hex_string = "0x"
371        add_to_data = False
372        finish_adding = False
373
374        # Find Software Identification Number
375        for element in soft_data:
376            n = len(element) - 1
377            while n >= 1:
378                if add_to_data:
379                    if element[n - 1 : n + 1] == "2a":
380                        finish_adding = True
381                        break
382                    if element[n - 1 : n + 1] != "0x":
383                        hex_string += element[n - 1 : n + 1]
384                        n -= 2
385                    else:
386                        n -= 2
387                else:
388                    if element[n - 3 : n + 1] == "2a31" or element[n - 3 : n + 1] == "2a01":
389                        add_to_data = True
390                        n -= 4
391                    else:
392                        n -= 4
393            if finish_adding:
394                break
395
396        if len(hex_string) <= 2:
397            logger.write_warning_to_html_report(
398                f"Software Identification: {hex_string} was not found in expected format"
399            )
400            return
401
402        software_identification = spn_types.ascii_map(int(hex_string, 16))
403
404        if re.search(r"[0-9]{2}\.[0-9]{2}\.[a-z]{2}\.[a-z]{2,}", software_identification) is None:
405            message = f"Software Identification: {software_identification} was not in expected format: MM.II.mm.aa.ee"
406            logger.write_failure_to_html_report(message)
407            pytest.fail(message)
408
409        logger.write_result_to_html_report(f"Software Identification: {software_identification}")
Description Validate Software Identification
GitHub Issue turnaroundfactor/BMS-HW-Test#394
Instructions 1. Request Software Identification
2. Validate Software Identification
3. Log Response
Estimated Duration 1 second
class TestBatteryRegulationInformation:
412class TestBatteryRegulationInformation:
413    """Retrieve and validate battery regulation information"""
414
415    def test_battery_regulation_information(self) -> None:
416        """
417        | Description          | Validate Battery Regulation Information 1 & 2                    |
418        | :------------------- | :--------------------------------------------------------------- |
419        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#392                                 |
420        | Instructions         | 1. Request Battery Regulation Information 1                 </br>\
421                                 2. Validate Information                                     </br>\
422                                 3. Log Response                                                  |
423        | Estimated Duration   | 2 seconds                                                        |
424        """
425
426        battery_regulation_one_request = CANFrame(pgn=PGN["RQST"], data=[0xFF03])
427        battery_regulation_two_request = CANFrame(pgn=PGN["RQST"], data=[0xFF04])
428
429        # Obtain Battery Regulation One Information
430        with CANBus(BATTERY_CHANNEL) as bus:
431            logger.write_result_to_html_report(
432                "<span style='font-weight: bold'>Battery Regulation Information 1 </span>"
433            )
434            if battery_frame := bus.process_call(battery_regulation_one_request):
435
436                if battery_frame.pgn.id != PGN["PropB_03", [32]].id:
437                    message = f"Expected {PGN['PropB_03', [32]].id} 0xFF03, but received PGN {battery_frame.pgn.id}"
438                    logger.write_warning_to_html_report(message)
439
440                for spn, elem in zip(PGN["PropB_03"].data_field, battery_frame.data):
441                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
442                    logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
443
444                    high_range = 3212.75
445                    low_range = 0.0
446                    if spn.name == "Battery Current":
447                        high_range = 1600.00
448                        low_range = -1600.00
449
450                    if not low_range <= elem <= high_range:
451                        logger.write_warning_to_html_report(
452                            f"{spn.name}: {cmp(elem, '>=', low_range)} and " f"{cmp(elem, '<=', high_range)}"
453                        )
454            else:
455                message = f"Did not receive response for PGN {PGN['PropB_03', [32]].id} (Battery Regulation One)"
456                logger.write_warning_to_html_report(message)
457
458        # Obtain Battery Regulation Two Information
459        with CANBus(BATTERY_CHANNEL) as bus:
460            logger.write_result_to_html_report(
461                "<span style='font-weight: bold'>Battery Regulation Information 2</span>"
462            )
463            if battery_frame_two := bus.process_call(battery_regulation_two_request):
464
465                if battery_frame_two.pgn.id != PGN["PropB_04", [32]].id:
466                    message = f"Expected PGN {PGN['PropB_04', [32]].id}, but received PGN {battery_frame_two.pgn.id}"
467                    logger.write_warning_to_html_report(message)
468
469                for spn, elem in zip(PGN["PropB_04"].data_field, battery_frame_two.data):
470
471                    if spn.name == "Reserved":
472                        continue
473
474                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
475                    logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
476
477                    high_range = 3
478                    if spn.name == "Bus Voltage Request":
479                        high_range = 3212.75
480
481                    if spn.name == "Transportability SOC":
482                        high_range = 100
483
484                    if not 0 <= elem <= high_range:
485                        logger.write_warning_to_html_report(
486                            f"{spn.name}: {cmp(elem, '>=', 0)} and {cmp(elem, '<=', high_range)}"
487                        )
488
489            else:
490                message = f"Did not receive response for PGN {PGN['PropB_04', [32]].id} (Battery Regulation Two)"
491                logger.write_warning_to_html_report(message)

Retrieve and validate battery regulation information

def test_battery_regulation_information(self) -> None:
415    def test_battery_regulation_information(self) -> None:
416        """
417        | Description          | Validate Battery Regulation Information 1 & 2                    |
418        | :------------------- | :--------------------------------------------------------------- |
419        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#392                                 |
420        | Instructions         | 1. Request Battery Regulation Information 1                 </br>\
421                                 2. Validate Information                                     </br>\
422                                 3. Log Response                                                  |
423        | Estimated Duration   | 2 seconds                                                        |
424        """
425
426        battery_regulation_one_request = CANFrame(pgn=PGN["RQST"], data=[0xFF03])
427        battery_regulation_two_request = CANFrame(pgn=PGN["RQST"], data=[0xFF04])
428
429        # Obtain Battery Regulation One Information
430        with CANBus(BATTERY_CHANNEL) as bus:
431            logger.write_result_to_html_report(
432                "<span style='font-weight: bold'>Battery Regulation Information 1 </span>"
433            )
434            if battery_frame := bus.process_call(battery_regulation_one_request):
435
436                if battery_frame.pgn.id != PGN["PropB_03", [32]].id:
437                    message = f"Expected {PGN['PropB_03', [32]].id} 0xFF03, but received PGN {battery_frame.pgn.id}"
438                    logger.write_warning_to_html_report(message)
439
440                for spn, elem in zip(PGN["PropB_03"].data_field, battery_frame.data):
441                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
442                    logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
443
444                    high_range = 3212.75
445                    low_range = 0.0
446                    if spn.name == "Battery Current":
447                        high_range = 1600.00
448                        low_range = -1600.00
449
450                    if not low_range <= elem <= high_range:
451                        logger.write_warning_to_html_report(
452                            f"{spn.name}: {cmp(elem, '>=', low_range)} and " f"{cmp(elem, '<=', high_range)}"
453                        )
454            else:
455                message = f"Did not receive response for PGN {PGN['PropB_03', [32]].id} (Battery Regulation One)"
456                logger.write_warning_to_html_report(message)
457
458        # Obtain Battery Regulation Two Information
459        with CANBus(BATTERY_CHANNEL) as bus:
460            logger.write_result_to_html_report(
461                "<span style='font-weight: bold'>Battery Regulation Information 2</span>"
462            )
463            if battery_frame_two := bus.process_call(battery_regulation_two_request):
464
465                if battery_frame_two.pgn.id != PGN["PropB_04", [32]].id:
466                    message = f"Expected PGN {PGN['PropB_04', [32]].id}, but received PGN {battery_frame_two.pgn.id}"
467                    logger.write_warning_to_html_report(message)
468
469                for spn, elem in zip(PGN["PropB_04"].data_field, battery_frame_two.data):
470
471                    if spn.name == "Reserved":
472                        continue
473
474                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
475                    logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
476
477                    high_range = 3
478                    if spn.name == "Bus Voltage Request":
479                        high_range = 3212.75
480
481                    if spn.name == "Transportability SOC":
482                        high_range = 100
483
484                    if not 0 <= elem <= high_range:
485                        logger.write_warning_to_html_report(
486                            f"{spn.name}: {cmp(elem, '>=', 0)} and {cmp(elem, '<=', high_range)}"
487                        )
488
489            else:
490                message = f"Did not receive response for PGN {PGN['PropB_04', [32]].id} (Battery Regulation Two)"
491                logger.write_warning_to_html_report(message)
Description Validate Battery Regulation Information 1 & 2
GitHub Issue turnaroundfactor/BMS-HW-Test#392
Instructions 1. Request Battery Regulation Information 1
2. Validate Information
3. Log Response
Estimated Duration 2 seconds
class TestConfigurationStateMessage:
494class TestConfigurationStateMessage:
495    """Confirms Configuration State Message matches profile after factory reset"""
496
497    def test_configuration_state_message(self) -> None:
498        """
499        | Description          | Validates if Configuration State Message matches profile         |
500        | :------------------- | :--------------------------------------------------------------- |
501        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#393                                 |
502        | Instructions         | 1. Request Configuration State                              </br>\
503                                 2. Check if Configuration State matches default values      </br>\
504                                 3. Log results                                                   |
505        | Estimated Duration   | 1 second                                                         |
506        """
507        failed_values = []
508        configuration_state_request = CANFrame(pgn=PGN["Request"], data=[PGN["Configuration State Message 1"].id])
509        with CANBus(BATTERY_CHANNEL) as bus:
510            if configuration_frame := bus.process_call(configuration_state_request):
511                if configuration_frame.pgn.id != PGN["Configuration State Message 1", [32]].id:
512                    message = (
513                        f"Received PGN {configuration_frame.pgn.id}, not PGN "
514                        f"{PGN['Configuration State Message 1', [32]].id} "
515                    )
516                    logger.write_failure_to_html_report(message)
517                    pytest.fail(message)
518
519                data = configuration_frame.data
520                pgn_data_field = PGN["Configuration State Message 1"].data_field
521
522                if len(data) != len(pgn_data_field):
523                    message = (
524                        f"Expected {len(pgn_data_field)} data fields, "
525                        f"got {len(data)}. Data is missing from response"
526                    )
527                    logger.write_failure_to_html_report(message)
528                    pytest.fail(message)
529
530                logger.write_result_to_html_report("<span style='font-weight: bold'>Configuration State Message</span>")
531
532                for spn, elem in zip(pgn_data_field, data):
533                    if spn.name == "Reserved":
534                        continue
535
536                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
537                    logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
538
539                    if not 0 <= elem <= 3:
540                        message = (
541                            f"{spn.name}: {cmp(elem, '>=', 0, f'({spn.data_type(elem)})')} and "
542                            f"{cmp(elem, '<=', 3, f'({spn.data_type(elem)})')}"
543                        )
544                        logger.write_warning_to_html_report(message)
545                        failed_values.append(spn.name)
546                        continue
547
548                    if spn.name in ("Battery Battle-override State", "Standby State", "Configure VPMS Function State"):
549                        message = (
550                            f"{spn.name}: {cmp(elem, '==', 0, f'({spn.data_type(elem)})', f'({spn.data_type(0)})')}"
551                        )
552                        if elem != 0:
553                            logger.write_warning_to_html_report(message)
554                            failed_values.append(spn.name)
555
556                    if spn.name in ("Automated Heater Function State", "Contactor(s) Control State"):
557                        message = (
558                            f"{spn.name}: {cmp(elem, '==', 1, f'({spn.data_type(elem)})', f'({spn.data_type(1)})')}"
559                        )
560                        if elem != 1:
561                            logger.write_warning_to_html_report(message)
562                            failed_values.append(spn.name)
563
564                if len(failed_values) > 0:
565                    categories = "data values" if len(failed_values) > 1 else "data value"
566                    message = (
567                        f"{len(failed_values)} {categories} failed "
568                        f"Configuration State Message specifications: {', '.join(failed_values)}"
569                    )
570                    logger.write_failure_to_html_report(message)
571                    pytest.fail(message)
572
573            else:
574                message = (
575                    f"Did not receive a response for PGN {PGN['Configuration State Message 1', [32]].id} "
576                    f"(Configuration State Message 1)"
577                )
578                logger.write_failure_to_html_report(message)
579                pytest.fail(message)

Confirms Configuration State Message matches profile after factory reset

def test_configuration_state_message(self) -> None:
497    def test_configuration_state_message(self) -> None:
498        """
499        | Description          | Validates if Configuration State Message matches profile         |
500        | :------------------- | :--------------------------------------------------------------- |
501        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#393                                 |
502        | Instructions         | 1. Request Configuration State                              </br>\
503                                 2. Check if Configuration State matches default values      </br>\
504                                 3. Log results                                                   |
505        | Estimated Duration   | 1 second                                                         |
506        """
507        failed_values = []
508        configuration_state_request = CANFrame(pgn=PGN["Request"], data=[PGN["Configuration State Message 1"].id])
509        with CANBus(BATTERY_CHANNEL) as bus:
510            if configuration_frame := bus.process_call(configuration_state_request):
511                if configuration_frame.pgn.id != PGN["Configuration State Message 1", [32]].id:
512                    message = (
513                        f"Received PGN {configuration_frame.pgn.id}, not PGN "
514                        f"{PGN['Configuration State Message 1', [32]].id} "
515                    )
516                    logger.write_failure_to_html_report(message)
517                    pytest.fail(message)
518
519                data = configuration_frame.data
520                pgn_data_field = PGN["Configuration State Message 1"].data_field
521
522                if len(data) != len(pgn_data_field):
523                    message = (
524                        f"Expected {len(pgn_data_field)} data fields, "
525                        f"got {len(data)}. Data is missing from response"
526                    )
527                    logger.write_failure_to_html_report(message)
528                    pytest.fail(message)
529
530                logger.write_result_to_html_report("<span style='font-weight: bold'>Configuration State Message</span>")
531
532                for spn, elem in zip(pgn_data_field, data):
533                    if spn.name == "Reserved":
534                        continue
535
536                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
537                    logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
538
539                    if not 0 <= elem <= 3:
540                        message = (
541                            f"{spn.name}: {cmp(elem, '>=', 0, f'({spn.data_type(elem)})')} and "
542                            f"{cmp(elem, '<=', 3, f'({spn.data_type(elem)})')}"
543                        )
544                        logger.write_warning_to_html_report(message)
545                        failed_values.append(spn.name)
546                        continue
547
548                    if spn.name in ("Battery Battle-override State", "Standby State", "Configure VPMS Function State"):
549                        message = (
550                            f"{spn.name}: {cmp(elem, '==', 0, f'({spn.data_type(elem)})', f'({spn.data_type(0)})')}"
551                        )
552                        if elem != 0:
553                            logger.write_warning_to_html_report(message)
554                            failed_values.append(spn.name)
555
556                    if spn.name in ("Automated Heater Function State", "Contactor(s) Control State"):
557                        message = (
558                            f"{spn.name}: {cmp(elem, '==', 1, f'({spn.data_type(elem)})', f'({spn.data_type(1)})')}"
559                        )
560                        if elem != 1:
561                            logger.write_warning_to_html_report(message)
562                            failed_values.append(spn.name)
563
564                if len(failed_values) > 0:
565                    categories = "data values" if len(failed_values) > 1 else "data value"
566                    message = (
567                        f"{len(failed_values)} {categories} failed "
568                        f"Configuration State Message specifications: {', '.join(failed_values)}"
569                    )
570                    logger.write_failure_to_html_report(message)
571                    pytest.fail(message)
572
573            else:
574                message = (
575                    f"Did not receive a response for PGN {PGN['Configuration State Message 1', [32]].id} "
576                    f"(Configuration State Message 1)"
577                )
578                logger.write_failure_to_html_report(message)
579                pytest.fail(message)
Description Validates if Configuration State Message matches profile
GitHub Issue turnaroundfactor/BMS-HW-Test#393
Instructions 1. Request Configuration State
2. Check if Configuration State matches default values
3. Log results
Estimated Duration 1 second
def na_maintenance_mode(bus: hitl_tester.modules.cyber_6t.canbus.CANBus) -> bool:
582def na_maintenance_mode(bus: CANBus) -> bool:
583    """This function will place battery in Maintenance Mode with Reset Value of "3"."""
584
585    maintenance_mode = CANFrame(  # Memory access only works in maintenance mode
586        destination_address=BATTERY_INFO.address,
587        pgn=PGN["PropA"],
588        data=[0, 0, 1, 1, 1, 3, 1, 3, 0, -1],
589    )
590
591    bus.send_message(maintenance_mode.message())  # Enter maintenance mode
592    if response := bus.read_input():
593        ack_frame = CANFrame.decode(response.arbitration_id, response.data)
594        if ack_frame.pgn.id != 0xE800 and ack_frame.data[0] != 0:
595            logger.write_warning_to_html_report("Unable to enter maintenance mode")
596            return False
597        return True
598
599    message = "Received no maintenance mode response"
600    logger.write_warning_to_html_report(message)
601    return False

This function will place battery in Maintenance Mode with Reset Value of "3".

class TestNameManagementMessage:
604class TestNameManagementMessage:
605    """This will test the mandatory NAME Management commands"""
606
607    def test_name_management_command(self) -> None:
608        """
609        | Description          | Test Name Management Command                                     |
610        | :------------------- | :--------------------------------------------------------------- |
611        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
612        | Instructions         | 1. Get NAME information from Address Claimed                </br>\
613                                 2. Change ECU value                                         </br>\
614                                 3. Test Name Management Command                             </br>\
615                                 4. Check Values updated                                     </br>\
616                                 5. Log results                                                   |
617        | Estimated Duration   | 1 second                                                         |
618        """
619
620        old_ecu_value = 0
621        new_ecu_value = 0
622        checksum_value = 0
623        adopt_name_request_data: list[float] = [255, 1, 1, 1, 1, 1, 1, 1, 1, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
624        address_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id])
625
626        with CANBus(BATTERY_CHANNEL) as bus:
627            na_maintenance_mode(bus)
628
629            # Name Management Test
630            if address_claimed := bus.process_call(address_request):
631                if address_claimed.pgn.id != PGN["Address Claimed", [32]].id:
632                    Errors.unexpected_packet("Address Claimed", address_claimed)
633                    message = f"Unexpected packet PGN {address_claimed.pgn.id} was received"
634                    logger.write_failure_to_html_report(message)
635                    pytest.fail(message)
636
637                checksum_value = sum(address_claimed.packed_data) & 0xFF
638                old_ecu_value = address_claimed.data[2]
639                if old_ecu_value > 0:
640                    new_ecu_value = 0
641                else:
642                    new_ecu_value = 1
643            else:
644                message = f"No response received for PGN {[PGN['Address Claimed', [32]].id]}"
645                logger.write_result_to_html_report(message)
646
647            if not checksum_value:
648                message = "Could not condense Name into bits for NAME Management"
649                logger.write_failure_to_html_report(message)
650                pytest.fail(message)
651
652            request_data: list[float] = [
653                checksum_value,
654                1,
655                0,
656                1,
657                1,
658                1,
659                1,
660                1,
661                1,
662                0,
663                1,
664                1,
665                new_ecu_value,
666                1,
667                1,
668                1,
669                1,
670                1,
671                1,
672                1,
673            ]
674            name_management_set_name = CANFrame(
675                destination_address=BATTERY_INFO.address, pgn=PGN["NAME Management Message"], data=request_data
676            )
677
678            if pending_response := bus.process_call(name_management_set_name):
679                if pending_response.pgn.id != PGN["NAME Management Message", [32]].id:
680                    if pending_response.pgn.id == PGN["Acknowledgement", [32]].id:
681                        message = (
682                            f"Battery may not support NAME Management, received PGN "
683                            f"{pending_response.pgn.id} ({spn_types.acknowledgement(pending_response.data[0])})"
684                        )
685                        logger.write_failure_to_html_report(message)
686                    else:
687                        Errors.unexpected_packet("NAME Management Message", pending_response)
688                        message = f"Unexpected PGN {pending_response.pgn.id} received"
689                        logger.write_failure_to_html_report(message)
690                    pytest.fail(message)
691
692                if pending_response.data[0] == 3:
693                    message = (
694                        f"Message was unsuccessful, received error: "
695                        f"{spn_types.name_error_code(pending_response.data[0])}"
696                    )
697                    logger.write_failure_to_html_report(message)
698                    pytest.fail(message)
699
700                if pending_response.data[12] != new_ecu_value:
701                    message = f"ECU Value was not changed from {old_ecu_value} to {new_ecu_value}"
702                    logger.write_failure_to_html_report(message)
703                    pytest.fail(message)
704            else:
705                message = (
706                    f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)"
707                )
708                logger.write_failure_to_html_report(message)
709                pytest.fail(message)
710
711            adopt_name_request = CANFrame(
712                destination_address=BATTERY_INFO.address,
713                pgn=PGN["NAME Management Message"],
714                data=adopt_name_request_data,
715            )
716
717            bus.send_message(adopt_name_request.message())
718
719            current_name_request_data: list[float] = [255, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
720            current_name_request = CANFrame(
721                destination_address=BATTERY_INFO.address,
722                pgn=PGN["NAME Management Message"],
723                data=current_name_request_data,
724            )
725
726            if name_management_response := bus.process_call(current_name_request):
727                if name_management_response.data[12] != new_ecu_value:
728                    message = (
729                        f"Name's ECU Instance was not updated after Name Management Changes, "
730                        f"{cmp(name_management_response.data[12], '==', new_ecu_value)}"
731                    )
732                    logger.write_failure_to_html_report(message)
733                    pytest.fail(message)
734            else:
735                message = (
736                    f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)"
737                )
738                logger.write_failure_to_html_report(message)
739                pytest.fail(message)
740
741            if new_address_claimed := bus.process_call(address_request):
742                if new_address_claimed.data[2] != new_ecu_value + 1:
743                    message = (
744                        f"Address Claimed was not updated after Name Management Changes. "
745                        f"{cmp(new_address_claimed.data[2], '==', new_ecu_value + 1)}"
746                    )
747                    logger.write_failure_to_html_report(message)
748                    pytest.fail(message)
749
750            logger.write_result_to_html_report(
751                f"Name Management Command was successful. "
752                f"ECU Instance was changed from {old_ecu_value} to {new_ecu_value}"
753            )
754
755    def test_wrong_name_management_data(self):
756        """
757        | Description          | Test Incorrect Data for NAME Management Command                  |
758        | :------------------- | :--------------------------------------------------------------- |
759        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
760        | Instructions         | 1. Provide wrong Checksum value for Name Command            </br>\
761                                 2. Check receive Checksum error code as response            </br>\
762                                 3. Log results                                                   |
763        | Estimated Duration   | 1 second                                                         |
764        """
765        wrong_checksum = 0
766        new_ecu_value = 0
767
768        current_name_request_data = [255, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
769        current_name_request = CANFrame(
770            destination_address=BATTERY_INFO.address,
771            pgn=PGN["NAME Management Message"],
772            data=current_name_request_data,
773        )
774
775        with CANBus(BATTERY_CHANNEL) as bus:
776            # Test Checksum Error -- Should return Error Code: 3
777            na_maintenance_mode(bus)
778
779            if current_name := bus.process_call(current_name_request):
780                if current_name.pgn.id != PGN["NAME Management Message", [32]].id:
781                    if current_name.pgn.id == PGN["Acknowledgement", [32]].id:
782                        message = (
783                            f"Battery may not support NAME Management, received PGN "
784                            f"{current_name.pgn.id} ({spn_types.acknowledgement(current_name.data[0])})"
785                        )
786                        logger.write_failure_to_html_report(message)
787                        pytest.fail(message)
788                    Errors.unexpected_packet("NAME Management Message", current_name)
789                    message = f"Unexpected packet was received: PGN {current_name.pgn.id}"
790                    logger.write_failure_to_html_report(message)
791                    pytest.fail(message)
792
793                wrong_checksum = sum(current_name.packed_data) & 0xFF
794                old_ecu_value = current_name.data[12]
795                if old_ecu_value > 0:
796                    new_ecu_value = 0
797                else:
798                    new_ecu_value = 1
799            else:
800                message = (
801                    f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)"
802                )
803                logger.write_failure_to_html_report(message)
804                pytest.fail(message)
805
806            request_data = [wrong_checksum, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, new_ecu_value, 1, 1, 1, 1, 1, 1, 1]
807            name_management_set_name = CANFrame(
808                destination_address=BATTERY_INFO.address, pgn=PGN["NAME Management Message"], data=request_data
809            )
810
811            if pending_response := bus.process_call(name_management_set_name):
812                if pending_response.pgn.id != PGN["NAME Management Message", [32]].id:
813                    if pending_response.pgn.id == PGN["Acknowledgement", [32]].id:
814                        message = (
815                            f"Battery may not support NAME Management, received PGN {pending_response.pgn.id} "
816                            f"({spn_types.acknowledgement(pending_response.data[0])})"
817                        )
818                        logger.write_failure_to_html_report(message)
819                        pytest.fail(message)
820                    Errors.unexpected_packet("NAME Management Message", pending_response)
821                    message = f"Unexpected packet was received: {pending_response.pgn.id}"
822                    logger.write_failure_to_html_report(message)
823                    pytest.fail(message)
824
825                if pending_response.data[0] != 3:
826                    message = cmp(
827                        pending_response.data[0],
828                        "==",
829                        3,
830                        f"({spn_types.name_error_code(pending_response.data[0])})",
831                        f"({spn_types.name_error_code(3)})",
832                    )
833                    logger.write_failure_to_html_report(
834                        f"PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message) "
835                        f"updated NAME Management with incorrect checksum value"
836                    )
837                    logger.write_failure_to_html_report(message)
838                    pytest.fail(message)
839            else:
840                message = (
841                    f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)"
842                )
843                logger.write_failure_to_html_report(message)
844                pytest.fail(message)
845
846            logger.write_result_to_html_report(
847                "NAME Management successfully did not process request with incorrect checksum value"
848            )

This will test the mandatory NAME Management commands

def test_name_management_command(self) -> None:
607    def test_name_management_command(self) -> None:
608        """
609        | Description          | Test Name Management Command                                     |
610        | :------------------- | :--------------------------------------------------------------- |
611        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
612        | Instructions         | 1. Get NAME information from Address Claimed                </br>\
613                                 2. Change ECU value                                         </br>\
614                                 3. Test Name Management Command                             </br>\
615                                 4. Check Values updated                                     </br>\
616                                 5. Log results                                                   |
617        | Estimated Duration   | 1 second                                                         |
618        """
619
620        old_ecu_value = 0
621        new_ecu_value = 0
622        checksum_value = 0
623        adopt_name_request_data: list[float] = [255, 1, 1, 1, 1, 1, 1, 1, 1, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
624        address_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id])
625
626        with CANBus(BATTERY_CHANNEL) as bus:
627            na_maintenance_mode(bus)
628
629            # Name Management Test
630            if address_claimed := bus.process_call(address_request):
631                if address_claimed.pgn.id != PGN["Address Claimed", [32]].id:
632                    Errors.unexpected_packet("Address Claimed", address_claimed)
633                    message = f"Unexpected packet PGN {address_claimed.pgn.id} was received"
634                    logger.write_failure_to_html_report(message)
635                    pytest.fail(message)
636
637                checksum_value = sum(address_claimed.packed_data) & 0xFF
638                old_ecu_value = address_claimed.data[2]
639                if old_ecu_value > 0:
640                    new_ecu_value = 0
641                else:
642                    new_ecu_value = 1
643            else:
644                message = f"No response received for PGN {[PGN['Address Claimed', [32]].id]}"
645                logger.write_result_to_html_report(message)
646
647            if not checksum_value:
648                message = "Could not condense Name into bits for NAME Management"
649                logger.write_failure_to_html_report(message)
650                pytest.fail(message)
651
652            request_data: list[float] = [
653                checksum_value,
654                1,
655                0,
656                1,
657                1,
658                1,
659                1,
660                1,
661                1,
662                0,
663                1,
664                1,
665                new_ecu_value,
666                1,
667                1,
668                1,
669                1,
670                1,
671                1,
672                1,
673            ]
674            name_management_set_name = CANFrame(
675                destination_address=BATTERY_INFO.address, pgn=PGN["NAME Management Message"], data=request_data
676            )
677
678            if pending_response := bus.process_call(name_management_set_name):
679                if pending_response.pgn.id != PGN["NAME Management Message", [32]].id:
680                    if pending_response.pgn.id == PGN["Acknowledgement", [32]].id:
681                        message = (
682                            f"Battery may not support NAME Management, received PGN "
683                            f"{pending_response.pgn.id} ({spn_types.acknowledgement(pending_response.data[0])})"
684                        )
685                        logger.write_failure_to_html_report(message)
686                    else:
687                        Errors.unexpected_packet("NAME Management Message", pending_response)
688                        message = f"Unexpected PGN {pending_response.pgn.id} received"
689                        logger.write_failure_to_html_report(message)
690                    pytest.fail(message)
691
692                if pending_response.data[0] == 3:
693                    message = (
694                        f"Message was unsuccessful, received error: "
695                        f"{spn_types.name_error_code(pending_response.data[0])}"
696                    )
697                    logger.write_failure_to_html_report(message)
698                    pytest.fail(message)
699
700                if pending_response.data[12] != new_ecu_value:
701                    message = f"ECU Value was not changed from {old_ecu_value} to {new_ecu_value}"
702                    logger.write_failure_to_html_report(message)
703                    pytest.fail(message)
704            else:
705                message = (
706                    f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)"
707                )
708                logger.write_failure_to_html_report(message)
709                pytest.fail(message)
710
711            adopt_name_request = CANFrame(
712                destination_address=BATTERY_INFO.address,
713                pgn=PGN["NAME Management Message"],
714                data=adopt_name_request_data,
715            )
716
717            bus.send_message(adopt_name_request.message())
718
719            current_name_request_data: list[float] = [255, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
720            current_name_request = CANFrame(
721                destination_address=BATTERY_INFO.address,
722                pgn=PGN["NAME Management Message"],
723                data=current_name_request_data,
724            )
725
726            if name_management_response := bus.process_call(current_name_request):
727                if name_management_response.data[12] != new_ecu_value:
728                    message = (
729                        f"Name's ECU Instance was not updated after Name Management Changes, "
730                        f"{cmp(name_management_response.data[12], '==', new_ecu_value)}"
731                    )
732                    logger.write_failure_to_html_report(message)
733                    pytest.fail(message)
734            else:
735                message = (
736                    f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)"
737                )
738                logger.write_failure_to_html_report(message)
739                pytest.fail(message)
740
741            if new_address_claimed := bus.process_call(address_request):
742                if new_address_claimed.data[2] != new_ecu_value + 1:
743                    message = (
744                        f"Address Claimed was not updated after Name Management Changes. "
745                        f"{cmp(new_address_claimed.data[2], '==', new_ecu_value + 1)}"
746                    )
747                    logger.write_failure_to_html_report(message)
748                    pytest.fail(message)
749
750            logger.write_result_to_html_report(
751                f"Name Management Command was successful. "
752                f"ECU Instance was changed from {old_ecu_value} to {new_ecu_value}"
753            )
Description Test Name Management Command
GitHub Issue turnaroundfactor/BMS-HW-Test#389
Instructions 1. Get NAME information from Address Claimed
2. Change ECU value
3. Test Name Management Command
4. Check Values updated
5. Log results
Estimated Duration 1 second
def test_wrong_name_management_data(self):
755    def test_wrong_name_management_data(self):
756        """
757        | Description          | Test Incorrect Data for NAME Management Command                  |
758        | :------------------- | :--------------------------------------------------------------- |
759        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
760        | Instructions         | 1. Provide wrong Checksum value for Name Command            </br>\
761                                 2. Check receive Checksum error code as response            </br>\
762                                 3. Log results                                                   |
763        | Estimated Duration   | 1 second                                                         |
764        """
765        wrong_checksum = 0
766        new_ecu_value = 0
767
768        current_name_request_data = [255, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
769        current_name_request = CANFrame(
770            destination_address=BATTERY_INFO.address,
771            pgn=PGN["NAME Management Message"],
772            data=current_name_request_data,
773        )
774
775        with CANBus(BATTERY_CHANNEL) as bus:
776            # Test Checksum Error -- Should return Error Code: 3
777            na_maintenance_mode(bus)
778
779            if current_name := bus.process_call(current_name_request):
780                if current_name.pgn.id != PGN["NAME Management Message", [32]].id:
781                    if current_name.pgn.id == PGN["Acknowledgement", [32]].id:
782                        message = (
783                            f"Battery may not support NAME Management, received PGN "
784                            f"{current_name.pgn.id} ({spn_types.acknowledgement(current_name.data[0])})"
785                        )
786                        logger.write_failure_to_html_report(message)
787                        pytest.fail(message)
788                    Errors.unexpected_packet("NAME Management Message", current_name)
789                    message = f"Unexpected packet was received: PGN {current_name.pgn.id}"
790                    logger.write_failure_to_html_report(message)
791                    pytest.fail(message)
792
793                wrong_checksum = sum(current_name.packed_data) & 0xFF
794                old_ecu_value = current_name.data[12]
795                if old_ecu_value > 0:
796                    new_ecu_value = 0
797                else:
798                    new_ecu_value = 1
799            else:
800                message = (
801                    f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)"
802                )
803                logger.write_failure_to_html_report(message)
804                pytest.fail(message)
805
806            request_data = [wrong_checksum, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, new_ecu_value, 1, 1, 1, 1, 1, 1, 1]
807            name_management_set_name = CANFrame(
808                destination_address=BATTERY_INFO.address, pgn=PGN["NAME Management Message"], data=request_data
809            )
810
811            if pending_response := bus.process_call(name_management_set_name):
812                if pending_response.pgn.id != PGN["NAME Management Message", [32]].id:
813                    if pending_response.pgn.id == PGN["Acknowledgement", [32]].id:
814                        message = (
815                            f"Battery may not support NAME Management, received PGN {pending_response.pgn.id} "
816                            f"({spn_types.acknowledgement(pending_response.data[0])})"
817                        )
818                        logger.write_failure_to_html_report(message)
819                        pytest.fail(message)
820                    Errors.unexpected_packet("NAME Management Message", pending_response)
821                    message = f"Unexpected packet was received: {pending_response.pgn.id}"
822                    logger.write_failure_to_html_report(message)
823                    pytest.fail(message)
824
825                if pending_response.data[0] != 3:
826                    message = cmp(
827                        pending_response.data[0],
828                        "==",
829                        3,
830                        f"({spn_types.name_error_code(pending_response.data[0])})",
831                        f"({spn_types.name_error_code(3)})",
832                    )
833                    logger.write_failure_to_html_report(
834                        f"PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message) "
835                        f"updated NAME Management with incorrect checksum value"
836                    )
837                    logger.write_failure_to_html_report(message)
838                    pytest.fail(message)
839            else:
840                message = (
841                    f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)"
842                )
843                logger.write_failure_to_html_report(message)
844                pytest.fail(message)
845
846            logger.write_result_to_html_report(
847                "NAME Management successfully did not process request with incorrect checksum value"
848            )
Description Test Incorrect Data for NAME Management Command
GitHub Issue turnaroundfactor/BMS-HW-Test#389
Instructions 1. Provide wrong Checksum value for Name Command
2. Check receive Checksum error code as response
3. Log results
Estimated Duration 1 second
class TestActiveDiagnosticTroubleCodes:
851class TestActiveDiagnosticTroubleCodes:
852    """Test that Active Diagnostic Trouble Codes are J1939 Compliant"""
853
854    def test_active_diagnostic_trouble_codes(self) -> None:
855        """
856        | Description          | Test Active Diagnostic Trouble Codes (DM1)                       |
857        | :------------------- | :--------------------------------------------------------------- |
858        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
859        | Instructions         | 1. Request Active Diagnostic Trouble Codes                  </br>\
860                                 2. Validate data is within specifications                   </br>\
861                                 3. Log results                                                   |
862        | Estimated Duration   | 1 second                                                         |
863        """
864        dm1_request = CANFrame(pgn=PGN["Request"], data=[PGN["Active Diagnostic Trouble Codes", [32]].id])
865        failed_values = []
866        with CANBus(BATTERY_CHANNEL) as bus:
867            if dm1_frame := bus.process_call(dm1_request):
868                if dm1_frame.pgn.id != PGN["Active Diagnostic Trouble Codes", [32]].id:
869                    Errors.unexpected_packet("Active Diagnostic Trouble Codes", dm1_frame)
870                    message = f"Unexpected data packet PGN {dm1_frame.pgn.id} was received"
871                    logger.write_failure_to_html_report(message)
872                    pytest.fail(message)
873
874                logger.write_result_to_html_report(
875                    "<span style='font-weight: bold'>Active Diagnostic Trouble Codes</span>"
876                )
877
878                dm1_data = dm1_frame.data
879                pgn_data_field = PGN["Active Diagnostic Trouble Codes"].data_field
880
881                for spn, elem in zip(pgn_data_field, dm1_data):
882                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
883                    logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
884                    high_value = 1
885
886                    if spn.name in (
887                        "Protect Lamp",
888                        "Amber Warning Lamp",
889                        "Red Stop Lamp",
890                        "Malfunction Indicator Lamp",
891                        "DTC1.SPN_Conversion_Method",
892                    ):
893                        high_value = 1
894
895                    if spn.name in (
896                        "Flash Protect Lamp",
897                        "Flash Amber Warning Lamp",
898                        "Flash Red Stop Lamp",
899                        "Flash Malfunction Indicator Lamp",
900                    ):
901                        high_value = 3
902
903                    if spn.name == "DTC1.Suspect_Parameter_Number":
904                        high_value = 524287
905
906                    if spn.name == "DTC1.Failure_Mode_Identifier":
907                        high_value = 31
908
909                    if spn.name == "DTC1.Occurrence_Count":
910                        high_value = 126
911
912                    if not 0 <= elem <= high_value:
913                        logger.write_warning_to_html_report(
914                            f"{spn.name}: {cmp(elem, '>=', 0, description)} and "
915                            f"{cmp(elem, '<=', high_value, description)}"
916                        )
917                        failed_values.append(spn.name)
918
919                if len(failed_values) > 0:
920                    categories = "data values" if len(failed_values) > 1 else "data value"
921                    message = (
922                        f"{len(failed_values)} {categories} failed "
923                        f"Active Diagnostic Trouble Code specifications: {', '.join(failed_values)}"
924                    )
925                    logger.write_failure_to_html_report(message)
926                    pytest.fail(message)
927
928            else:
929                message = (
930                    f"No response was received for mandatory command: "
931                    f"PGN {PGN['Active Diagnostic Trouble Codes', [32]].id} (Active Diagnostic Trouble Codes)"
932                )
933                logger.write_failure_to_html_report(message)
934                pytest.fail(message)

Test that Active Diagnostic Trouble Codes are J1939 Compliant

def test_active_diagnostic_trouble_codes(self) -> None:
854    def test_active_diagnostic_trouble_codes(self) -> None:
855        """
856        | Description          | Test Active Diagnostic Trouble Codes (DM1)                       |
857        | :------------------- | :--------------------------------------------------------------- |
858        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
859        | Instructions         | 1. Request Active Diagnostic Trouble Codes                  </br>\
860                                 2. Validate data is within specifications                   </br>\
861                                 3. Log results                                                   |
862        | Estimated Duration   | 1 second                                                         |
863        """
864        dm1_request = CANFrame(pgn=PGN["Request"], data=[PGN["Active Diagnostic Trouble Codes", [32]].id])
865        failed_values = []
866        with CANBus(BATTERY_CHANNEL) as bus:
867            if dm1_frame := bus.process_call(dm1_request):
868                if dm1_frame.pgn.id != PGN["Active Diagnostic Trouble Codes", [32]].id:
869                    Errors.unexpected_packet("Active Diagnostic Trouble Codes", dm1_frame)
870                    message = f"Unexpected data packet PGN {dm1_frame.pgn.id} was received"
871                    logger.write_failure_to_html_report(message)
872                    pytest.fail(message)
873
874                logger.write_result_to_html_report(
875                    "<span style='font-weight: bold'>Active Diagnostic Trouble Codes</span>"
876                )
877
878                dm1_data = dm1_frame.data
879                pgn_data_field = PGN["Active Diagnostic Trouble Codes"].data_field
880
881                for spn, elem in zip(pgn_data_field, dm1_data):
882                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
883                    logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
884                    high_value = 1
885
886                    if spn.name in (
887                        "Protect Lamp",
888                        "Amber Warning Lamp",
889                        "Red Stop Lamp",
890                        "Malfunction Indicator Lamp",
891                        "DTC1.SPN_Conversion_Method",
892                    ):
893                        high_value = 1
894
895                    if spn.name in (
896                        "Flash Protect Lamp",
897                        "Flash Amber Warning Lamp",
898                        "Flash Red Stop Lamp",
899                        "Flash Malfunction Indicator Lamp",
900                    ):
901                        high_value = 3
902
903                    if spn.name == "DTC1.Suspect_Parameter_Number":
904                        high_value = 524287
905
906                    if spn.name == "DTC1.Failure_Mode_Identifier":
907                        high_value = 31
908
909                    if spn.name == "DTC1.Occurrence_Count":
910                        high_value = 126
911
912                    if not 0 <= elem <= high_value:
913                        logger.write_warning_to_html_report(
914                            f"{spn.name}: {cmp(elem, '>=', 0, description)} and "
915                            f"{cmp(elem, '<=', high_value, description)}"
916                        )
917                        failed_values.append(spn.name)
918
919                if len(failed_values) > 0:
920                    categories = "data values" if len(failed_values) > 1 else "data value"
921                    message = (
922                        f"{len(failed_values)} {categories} failed "
923                        f"Active Diagnostic Trouble Code specifications: {', '.join(failed_values)}"
924                    )
925                    logger.write_failure_to_html_report(message)
926                    pytest.fail(message)
927
928            else:
929                message = (
930                    f"No response was received for mandatory command: "
931                    f"PGN {PGN['Active Diagnostic Trouble Codes', [32]].id} (Active Diagnostic Trouble Codes)"
932                )
933                logger.write_failure_to_html_report(message)
934                pytest.fail(message)
Description Test Active Diagnostic Trouble Codes (DM1)
GitHub Issue turnaroundfactor/BMS-HW-Test#389
Instructions 1. Request Active Diagnostic Trouble Codes
2. Validate data is within specifications
3. Log results
Estimated Duration 1 second
class TestVehicleElectricalPower:
1063class TestVehicleElectricalPower:
1064    """Test Vehicle Electrical Power command is J1939 Compliant"""
1065
1066    def test_vehicle_electrical_power(self) -> None:
1067        """
1068        | Description          | Test Vehicle Electrical Power #5 (VEP5) command                  |
1069        | :------------------- | :--------------------------------------------------------------- |
1070        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
1071        | Instructions         | 1. Send Vehicle Electrical Power #5 command                 </br>\
1072                                 2. Validate data is within specifications                   </br>\
1073                                 3. Log response                                                  |
1074        | Estimated Duration   | 1 second                                                         |
1075        """
1076        failed_values = []
1077        with CANBus(BATTERY_CHANNEL) as bus:
1078            vehicle_electrical_power_request = CANFrame(
1079                destination_address=BATTERY_INFO.address,
1080                pgn=PGN["Request"],
1081                data=[PGN["Vehicle Electrical Power #5"].id],
1082            )
1083            if vehicle_electrical_power_response := bus.process_call(vehicle_electrical_power_request):
1084                if vehicle_electrical_power_response.pgn.id != PGN["Vehicle Electrical Power #5", [32]].id:
1085                    if vehicle_electrical_power_response.pgn.id == 59392:
1086                        message = (
1087                            f"Received PGN {vehicle_electrical_power_response.pgn.id} "
1088                            f"({spn_types.acknowledgement(vehicle_electrical_power_response.data[0])}), "
1089                            f'not PGN {PGN["Vehicle Electrical Power #5", [32]].id} '
1090                        )
1091                    else:
1092                        message = (
1093                            f"Received PGN {vehicle_electrical_power_response.pgn.id} "
1094                            f"{vehicle_electrical_power_response.pgn.short_name}, "
1095                            f'not PGN {PGN["Vehicle Electrical Power #5", [32]].id} '
1096                        )
1097                    logger.write_failure_to_html_report(message)
1098                    pytest.fail(message)
1099            else:
1100                message = (
1101                    f"Battery did not respond to PGN {PGN['Vehicle Electrical Power #5', [32]].id} "
1102                    f"Vehicle Electrical Power #5 request"
1103                )
1104                logger.write_failure_to_html_report(message)
1105                pytest.fail(message)
1106
1107            vep5_data = vehicle_electrical_power_response.data
1108            vep5_pgn = PGN["Vehicle Electrical Power #5"].data_field
1109
1110            logger.write_result_to_html_report("<span style='font-weight: bold'>Vehicle Electrical Power #5</span>")
1111
1112            for spn, elem in zip(vep5_pgn, vep5_data):
1113                if spn.name == "Reserved":
1114                    continue
1115
1116                description = f"({desc})" if (desc := spn.data_type(elem)) else ""
1117                logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
1118                high_value = 1.0
1119                if spn.name == "SLI Battery Pack State of Charge":
1120                    high_value = 160.6375
1121
1122                if spn.name == "SLI Battery Pack Capacity":
1123                    high_value = 64255
1124
1125                if spn.name == "SLI Battery Pack Health":
1126                    high_value = 125
1127
1128                if spn.name == "SLI Cranking Predicted Minimum Battery Voltage":
1129                    high_value = 50
1130
1131                if not 0 <= elem <= high_value:
1132                    logger.write_warning_to_html_report(
1133                        f"{spn.name}: {cmp(elem, '>=', 0, description)} and "
1134                        f"{cmp(elem, '<=', high_value, description)}"
1135                    )
1136                    failed_values.append(spn.name)
1137
1138            if len(failed_values) > 0:
1139                categories = "data values" if len(failed_values) > 1 else "data value"
1140                message = (
1141                    f"{len(failed_values)} {categories} failed "
1142                    f"Configuration State Message specifications: {', '.join(failed_values)}"
1143                )
1144                logger.write_failure_to_html_report(message)
1145                pytest.fail(message)
1146
1147            logger.write_result_to_html_report("Test Vehicle Electrical Power #5 command was successful")

Test Vehicle Electrical Power command is J1939 Compliant

def test_vehicle_electrical_power(self) -> None:
1066    def test_vehicle_electrical_power(self) -> None:
1067        """
1068        | Description          | Test Vehicle Electrical Power #5 (VEP5) command                  |
1069        | :------------------- | :--------------------------------------------------------------- |
1070        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#389                                 |
1071        | Instructions         | 1. Send Vehicle Electrical Power #5 command                 </br>\
1072                                 2. Validate data is within specifications                   </br>\
1073                                 3. Log response                                                  |
1074        | Estimated Duration   | 1 second                                                         |
1075        """
1076        failed_values = []
1077        with CANBus(BATTERY_CHANNEL) as bus:
1078            vehicle_electrical_power_request = CANFrame(
1079                destination_address=BATTERY_INFO.address,
1080                pgn=PGN["Request"],
1081                data=[PGN["Vehicle Electrical Power #5"].id],
1082            )
1083            if vehicle_electrical_power_response := bus.process_call(vehicle_electrical_power_request):
1084                if vehicle_electrical_power_response.pgn.id != PGN["Vehicle Electrical Power #5", [32]].id:
1085                    if vehicle_electrical_power_response.pgn.id == 59392:
1086                        message = (
1087                            f"Received PGN {vehicle_electrical_power_response.pgn.id} "
1088                            f"({spn_types.acknowledgement(vehicle_electrical_power_response.data[0])}), "
1089                            f'not PGN {PGN["Vehicle Electrical Power #5", [32]].id} '
1090                        )
1091                    else:
1092                        message = (
1093                            f"Received PGN {vehicle_electrical_power_response.pgn.id} "
1094                            f"{vehicle_electrical_power_response.pgn.short_name}, "
1095                            f'not PGN {PGN["Vehicle Electrical Power #5", [32]].id} '
1096                        )
1097                    logger.write_failure_to_html_report(message)
1098                    pytest.fail(message)
1099            else:
1100                message = (
1101                    f"Battery did not respond to PGN {PGN['Vehicle Electrical Power #5', [32]].id} "
1102                    f"Vehicle Electrical Power #5 request"
1103                )
1104                logger.write_failure_to_html_report(message)
1105                pytest.fail(message)
1106
1107            vep5_data = vehicle_electrical_power_response.data
1108            vep5_pgn = PGN["Vehicle Electrical Power #5"].data_field
1109
1110            logger.write_result_to_html_report("<span style='font-weight: bold'>Vehicle Electrical Power #5</span>")
1111
1112            for spn, elem in zip(vep5_pgn, vep5_data):
1113                if spn.name == "Reserved":
1114                    continue
1115
1116                description = f"({desc})" if (desc := spn.data_type(elem)) else ""
1117                logger.write_result_to_html_report(f"{spn.name}: {elem} {description}")
1118                high_value = 1.0
1119                if spn.name == "SLI Battery Pack State of Charge":
1120                    high_value = 160.6375
1121
1122                if spn.name == "SLI Battery Pack Capacity":
1123                    high_value = 64255
1124
1125                if spn.name == "SLI Battery Pack Health":
1126                    high_value = 125
1127
1128                if spn.name == "SLI Cranking Predicted Minimum Battery Voltage":
1129                    high_value = 50
1130
1131                if not 0 <= elem <= high_value:
1132                    logger.write_warning_to_html_report(
1133                        f"{spn.name}: {cmp(elem, '>=', 0, description)} and "
1134                        f"{cmp(elem, '<=', high_value, description)}"
1135                    )
1136                    failed_values.append(spn.name)
1137
1138            if len(failed_values) > 0:
1139                categories = "data values" if len(failed_values) > 1 else "data value"
1140                message = (
1141                    f"{len(failed_values)} {categories} failed "
1142                    f"Configuration State Message specifications: {', '.join(failed_values)}"
1143                )
1144                logger.write_failure_to_html_report(message)
1145                pytest.fail(message)
1146
1147            logger.write_result_to_html_report("Test Vehicle Electrical Power #5 command was successful")
Description Test Vehicle Electrical Power #5 (VEP5) command
GitHub Issue turnaroundfactor/BMS-HW-Test#389
Instructions 1. Send Vehicle Electrical Power #5 command
2. Validate data is within specifications
3. Log response
Estimated Duration 1 second
class TestManufacturerCommands:
1150class TestManufacturerCommands:
1151    """Test Manufacturer Commands are compliant with specs"""
1152
1153    def saft_commands_test(self):
1154        """Tests SAFT Manufacturer Commands"""
1155        with CANBus(BATTERY_CHANNEL) as bus:
1156            manufactured_command_request = CANFrame(destination_address=BATTERY_INFO.address, pgn=PGN["RQST"], data=[0])
1157            invalid_response = []
1158            for address in itertools.chain(
1159                [0xFFD2], range(0xFFD4, 0xFFD9), range(0xFFDC, 0xFFDF), range(0xFFE0, 0xFFE2), [0xFFE4]
1160            ):
1161                manufactured_command_request.data = [address]
1162                default_pgn = PGN[address]
1163                pgn_name = default_pgn.name
1164                logger.write_result_to_html_report(
1165                    f"<span style='font-weight: bold'>PGN {address} ({pgn_name})---</span>"
1166                )
1167
1168                if response_frame := bus.process_call(manufactured_command_request):
1169                    if not response_frame.pgn.id == default_pgn.id:
1170
1171                        if response_frame.pgn.id == PGN["ACKM", [32]].id:
1172                            message = (
1173                                f"Expected {address} ({pgn_name}): Received PGN {response_frame.pgn.id} "
1174                                f"({spn_types.acknowledgement(response_frame.data[0])}) "
1175                            )
1176                            logger.write_warning_to_html_report(message)
1177                        else:
1178                            logger.write_warning_to_html_report(
1179                                f"Expected PGN {address} ({pgn_name}), but received "
1180                                f"{response_frame.pgn.id} ({response_frame.pgn.name}). "
1181                                f"Unable to complete check for command"
1182                            )
1183                        invalid_response.append(f"PGN {address} ({pgn_name})")
1184                        continue
1185                else:
1186                    message = f"Did not receive response from PGN {address} {pgn_name}"
1187                    logger.write_warning_to_html_report(message)
1188                    invalid_response.append(f"PGN {address} ({pgn_name})")
1189
1190                    continue
1191
1192                if response_frame.priority != default_pgn.default_priority:
1193                    message = (
1194                        f"Expected priority level of {default_pgn.default_priority}"
1195                        f" but got priority level {response_frame.priority} for PGN {address}, {pgn_name}"
1196                    )
1197                    invalid_response.append(f"PGN {address} {pgn_name}")
1198                    logger.write_warning_to_html_report(message)
1199
1200                if len(response_frame.packed_data) != 8:
1201                    message = (
1202                        f"Unexpected data length for PGN {address}, {pgn_name}. Expected length of 8, "
1203                        f"received {len(response_frame.packed_data)}"
1204                    )
1205                    logger.write_warning_to_html_report(message)
1206
1207                not_passed_elem = []
1208                for spn, elem in zip(default_pgn.data_field, response_frame.data):
1209                    low_range = 0
1210                    high_range = 3
1211                    spn_name = spn.name
1212                    if spn.name == "Reserved":
1213                        continue
1214
1215                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
1216
1217                    logger.write_result_to_html_report(f"{spn_name}: {elem}{description}")
1218
1219                    if pgn_name == "Battery ECU Status":
1220                        if spn_name in ("Battery Mode", "FET Array State", "SOC Mode", "Heat Reason"):
1221                            high_range = 7
1222                        elif spn_name == "Long-Term Fault Log Status":
1223                            high_range = 15
1224                        elif spn_name == "Software Part Number":
1225                            high_range = 64255
1226
1227                    if pgn_name in ("Battery Cell Status 1", "Battery Cell Status 2"):
1228                        high_range = 6.4255
1229
1230                    if pgn_name == "Battery Performance":
1231                        if spn_name == "Battery Current":
1232                            low_range = -82000.00
1233                            high_range = 82495.35
1234                        if spn_name == "Internal State of Health":
1235                            low_range = -204.800
1236                            high_range = 204.775
1237
1238                    if pgn_name == "Battery Temperatures":
1239                        if spn_name == "MCU Temperature":
1240                            low_range = -40
1241                            high_range = 210
1242                        else:
1243                            low_range = -50
1244                            high_range = 200
1245
1246                    if pgn_name == "Battery Balancing Circuit Info":
1247                        if spn.name == "Cell Voltage Difference":
1248                            high_range = 6.4255
1249                        if spn_name == "Cell Voltage Sum":
1250                            high_range = 104.8576
1251
1252                    if pgn_name in ("Battery Cell Upper SOC", "Battery Cell Lower SOC"):
1253                        low_range = -10
1254                        high_range = 115
1255
1256                    if pgn_name == "Battery Function Status":
1257                        if spn_name == "Heater Set Point":
1258                            low_range = -50
1259                            high_range = 25
1260                        if spn_name == "Storage Delay Time Limit":
1261                            high_range = 65535
1262                        if spn_name == "Last Storage Duration (Minutes)":
1263                            high_range = 59
1264                        if spn_name == "Last Storage Duration (Hours)":
1265                            high_range = 23
1266                        if spn_name == "Last Storage Duration (Days)":
1267                            high_range = 31
1268                        if spn_name == "Last Storage Duration (Months)":
1269                            high_range = 255
1270                        if spn_name == "Effective Reset Time":
1271                            high_range = 60
1272
1273                    if not low_range <= elem <= high_range:
1274                        logger.write_warning_to_html_report(
1275                            f"{spn.name}: {cmp(elem, '>=', 0, description)} and "
1276                            f"{cmp(elem, '<=', high_range, description)}"
1277                        )
1278                        not_passed_elem.append(spn_name)
1279
1280                if len(not_passed_elem) == 0:
1281                    message = f"✅ All data fields in PGN {default_pgn.id} ({pgn_name}) met requirements"
1282                    logger.write_result_to_html_report(message)
1283
1284            if len(invalid_response) > 0:
1285                message = (
1286                    f"{len(invalid_response)} SAFT Manufacturer Command{'s' if len(invalid_response) > 1 else ''} "
1287                    f"failed: {', '.join(invalid_response)}"
1288                )
1289                logger.write_failure_to_html_report(message)
1290                pytest.fail(message)
1291            else:
1292                logger.write_result_to_html_report("All SAFT Manufacturer Commands passed")
1293
1294    def test_manufacturer_commands(self) -> None:
1295        """
1296        | Description          | Fingerprint Manufacturer Commands                                |
1297        | :------------------- | :--------------------------------------------------------------- |
1298        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#388                                 |
1299        | Instructions         | 1. Send request for manufacturer command                    </br>\
1300                                 2. Check values in data                                     </br>\
1301                                 3. Log response                                                  |
1302        | Estimated Duration   | 2 seconds                                                        |
1303        """
1304
1305        if BATTERY_INFO.manufacturer_code == ManufacturerID.SAFT:
1306            self.saft_commands_test()
1307        else:
1308            message = "No known manufacturer commands to test"
1309            logger.write_result_to_html_report(message)
1310            pytest.skip(message)

Test Manufacturer Commands are compliant with specs

def saft_commands_test(self):
1153    def saft_commands_test(self):
1154        """Tests SAFT Manufacturer Commands"""
1155        with CANBus(BATTERY_CHANNEL) as bus:
1156            manufactured_command_request = CANFrame(destination_address=BATTERY_INFO.address, pgn=PGN["RQST"], data=[0])
1157            invalid_response = []
1158            for address in itertools.chain(
1159                [0xFFD2], range(0xFFD4, 0xFFD9), range(0xFFDC, 0xFFDF), range(0xFFE0, 0xFFE2), [0xFFE4]
1160            ):
1161                manufactured_command_request.data = [address]
1162                default_pgn = PGN[address]
1163                pgn_name = default_pgn.name
1164                logger.write_result_to_html_report(
1165                    f"<span style='font-weight: bold'>PGN {address} ({pgn_name})---</span>"
1166                )
1167
1168                if response_frame := bus.process_call(manufactured_command_request):
1169                    if not response_frame.pgn.id == default_pgn.id:
1170
1171                        if response_frame.pgn.id == PGN["ACKM", [32]].id:
1172                            message = (
1173                                f"Expected {address} ({pgn_name}): Received PGN {response_frame.pgn.id} "
1174                                f"({spn_types.acknowledgement(response_frame.data[0])}) "
1175                            )
1176                            logger.write_warning_to_html_report(message)
1177                        else:
1178                            logger.write_warning_to_html_report(
1179                                f"Expected PGN {address} ({pgn_name}), but received "
1180                                f"{response_frame.pgn.id} ({response_frame.pgn.name}). "
1181                                f"Unable to complete check for command"
1182                            )
1183                        invalid_response.append(f"PGN {address} ({pgn_name})")
1184                        continue
1185                else:
1186                    message = f"Did not receive response from PGN {address} {pgn_name}"
1187                    logger.write_warning_to_html_report(message)
1188                    invalid_response.append(f"PGN {address} ({pgn_name})")
1189
1190                    continue
1191
1192                if response_frame.priority != default_pgn.default_priority:
1193                    message = (
1194                        f"Expected priority level of {default_pgn.default_priority}"
1195                        f" but got priority level {response_frame.priority} for PGN {address}, {pgn_name}"
1196                    )
1197                    invalid_response.append(f"PGN {address} {pgn_name}")
1198                    logger.write_warning_to_html_report(message)
1199
1200                if len(response_frame.packed_data) != 8:
1201                    message = (
1202                        f"Unexpected data length for PGN {address}, {pgn_name}. Expected length of 8, "
1203                        f"received {len(response_frame.packed_data)}"
1204                    )
1205                    logger.write_warning_to_html_report(message)
1206
1207                not_passed_elem = []
1208                for spn, elem in zip(default_pgn.data_field, response_frame.data):
1209                    low_range = 0
1210                    high_range = 3
1211                    spn_name = spn.name
1212                    if spn.name == "Reserved":
1213                        continue
1214
1215                    description = f"({desc})" if (desc := spn.data_type(elem)) else ""
1216
1217                    logger.write_result_to_html_report(f"{spn_name}: {elem}{description}")
1218
1219                    if pgn_name == "Battery ECU Status":
1220                        if spn_name in ("Battery Mode", "FET Array State", "SOC Mode", "Heat Reason"):
1221                            high_range = 7
1222                        elif spn_name == "Long-Term Fault Log Status":
1223                            high_range = 15
1224                        elif spn_name == "Software Part Number":
1225                            high_range = 64255
1226
1227                    if pgn_name in ("Battery Cell Status 1", "Battery Cell Status 2"):
1228                        high_range = 6.4255
1229
1230                    if pgn_name == "Battery Performance":
1231                        if spn_name == "Battery Current":
1232                            low_range = -82000.00
1233                            high_range = 82495.35
1234                        if spn_name == "Internal State of Health":
1235                            low_range = -204.800
1236                            high_range = 204.775
1237
1238                    if pgn_name == "Battery Temperatures":
1239                        if spn_name == "MCU Temperature":
1240                            low_range = -40
1241                            high_range = 210
1242                        else:
1243                            low_range = -50
1244                            high_range = 200
1245
1246                    if pgn_name == "Battery Balancing Circuit Info":
1247                        if spn.name == "Cell Voltage Difference":
1248                            high_range = 6.4255
1249                        if spn_name == "Cell Voltage Sum":
1250                            high_range = 104.8576
1251
1252                    if pgn_name in ("Battery Cell Upper SOC", "Battery Cell Lower SOC"):
1253                        low_range = -10
1254                        high_range = 115
1255
1256                    if pgn_name == "Battery Function Status":
1257                        if spn_name == "Heater Set Point":
1258                            low_range = -50
1259                            high_range = 25
1260                        if spn_name == "Storage Delay Time Limit":
1261                            high_range = 65535
1262                        if spn_name == "Last Storage Duration (Minutes)":
1263                            high_range = 59
1264                        if spn_name == "Last Storage Duration (Hours)":
1265                            high_range = 23
1266                        if spn_name == "Last Storage Duration (Days)":
1267                            high_range = 31
1268                        if spn_name == "Last Storage Duration (Months)":
1269                            high_range = 255
1270                        if spn_name == "Effective Reset Time":
1271                            high_range = 60
1272
1273                    if not low_range <= elem <= high_range:
1274                        logger.write_warning_to_html_report(
1275                            f"{spn.name}: {cmp(elem, '>=', 0, description)} and "
1276                            f"{cmp(elem, '<=', high_range, description)}"
1277                        )
1278                        not_passed_elem.append(spn_name)
1279
1280                if len(not_passed_elem) == 0:
1281                    message = f"✅ All data fields in PGN {default_pgn.id} ({pgn_name}) met requirements"
1282                    logger.write_result_to_html_report(message)
1283
1284            if len(invalid_response) > 0:
1285                message = (
1286                    f"{len(invalid_response)} SAFT Manufacturer Command{'s' if len(invalid_response) > 1 else ''} "
1287                    f"failed: {', '.join(invalid_response)}"
1288                )
1289                logger.write_failure_to_html_report(message)
1290                pytest.fail(message)
1291            else:
1292                logger.write_result_to_html_report("All SAFT Manufacturer Commands passed")

Tests SAFT Manufacturer Commands

def test_manufacturer_commands(self) -> None:
1294    def test_manufacturer_commands(self) -> None:
1295        """
1296        | Description          | Fingerprint Manufacturer Commands                                |
1297        | :------------------- | :--------------------------------------------------------------- |
1298        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#388                                 |
1299        | Instructions         | 1. Send request for manufacturer command                    </br>\
1300                                 2. Check values in data                                     </br>\
1301                                 3. Log response                                                  |
1302        | Estimated Duration   | 2 seconds                                                        |
1303        """
1304
1305        if BATTERY_INFO.manufacturer_code == ManufacturerID.SAFT:
1306            self.saft_commands_test()
1307        else:
1308            message = "No known manufacturer commands to test"
1309            logger.write_result_to_html_report(message)
1310            pytest.skip(message)
Description Fingerprint Manufacturer Commands
GitHub Issue turnaroundfactor/BMS-HW-Test#388
Instructions 1. Send request for manufacturer command
2. Check values in data
3. Log response
Estimated Duration 2 seconds
class TestECUInformation:
1436class TestECUInformation:
1437    """Gets information about the physical ECU and its hardware"""
1438
1439    @staticmethod
1440    def bytes_to_ascii(bs: list[float]) -> list[str]:
1441        """Converts bytes to ASCII string"""
1442        s: str = ""
1443        for b in bs:
1444            h = re.sub(r"^[^0-9a-fA-F]+$", "", f"{b:x}")
1445            try:
1446                ba = bytearray.fromhex(h)[::-1]
1447                s += ba.decode("utf-8", "ignore")
1448                s = re.sub(r"[^\x20-\x7E]", "", s)
1449            except ValueError:
1450                # NOTE: This will ignore any invalid packets (from BrenTronics)
1451                logger.write_warning_to_report(f"Skipping invalid hex: {b:x}")
1452        return list(filter(None, s.split("*")))
1453
1454    def test_ecu_information(self) -> None:
1455        """
1456        | Description          | Get information from ECUID response                              |
1457        | :------------------- | :--------------------------------------------------------------- |
1458        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#395                                 |
1459        | Instructions         | 1. Request ECUID data                                       </br>\
1460                                 2. Validate data is within specifications                   </br>\
1461                                 3. Log response                                                  |
1462        | Estimated Duration   | 10 seconds                                                       |
1463        """
1464
1465        info = []
1466        with CANBus(BATTERY_CHANNEL) as bus:
1467            ecu_request = CANFrame(pgn=PGN["Request"], data=[PGN["ECUID"].id])
1468            if tp_cm_frame := bus.process_call(ecu_request):
1469                if tp_cm_frame is not None:
1470                    if tp_cm_frame.pgn.id == PGN["TP.CM", [32]].id:
1471                        data = []
1472                        cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0])
1473                        if data_frame := bus.process_call(cts_request):
1474                            if data_frame is not None:
1475                                if data_frame.pgn.id == PGN["TP.DT"].id:
1476                                    packet_count = int(tp_cm_frame.data[2])
1477                                    # NOTE: This will ignore the invalid header packet from BrenTronics
1478                                    if BATTERY_INFO.manufacturer_code != ManufacturerID.BRENTRONICS:
1479                                        data.append(data_frame.data[1])
1480                                        packet_count -= 1
1481                                    for _ in range(packet_count):
1482                                        data_message = bus.read_input()
1483                                        if data_message is not None:
1484                                            frame = CANFrame.decode(data_message.arbitration_id, data_message.data)
1485                                            if frame.pgn.id == PGN["TP.DT"].id:
1486                                                data.append(frame.data[1])
1487                                            else:
1488                                                Errors.unexpected_packet("TP.DT", frame)
1489                                                break
1490                                        else:
1491                                            Errors.no_packet("TP.DT")
1492                                            break
1493                                    info = self.bytes_to_ascii(data)
1494                                    eom_request = CANFrame(
1495                                        pgn=PGN["TP.CM"],
1496                                        data=[17, tp_cm_frame.data[1], packet_count, 0xFF, tp_cm_frame.data[-1]],
1497                                    )
1498                                    if eom_frame := bus.process_call(eom_request):
1499                                        if eom_frame is not None:
1500                                            if eom_frame.pgn.id == PGN["DM15"].id:
1501                                                if eom_frame.data[2] == 4:  # Operation Completed
1502                                                    logger.write_info_to_report("ECUID data transfer successful")
1503                                                else:
1504                                                    logger.write_warning_to_html_report("Unsuccessful EOM response")
1505                                            else:
1506                                                Errors.unexpected_packet("DM15", eom_frame)
1507                                        else:
1508                                            Errors.no_packet("DM15")
1509                                    else:
1510                                        # timeout
1511                                        logger.write_warning_to_report("No response after sending EOM (DM15)")
1512                                else:
1513                                    Errors.unexpected_packet("TP.DT", data_frame)
1514                            else:
1515                                Errors.no_packet("TP.DT")
1516                        else:
1517                            message = f"Did not receive response from PGN {PGN['TP.CM', [32]].id} (TP.CM)"
1518                            logger.write_failure_to_html_report(message)
1519                            pytest.fail(message)
1520                    else:
1521                        Errors.unexpected_packet("TP.CM", tp_cm_frame)
1522                else:
1523                    Errors.no_packet("TP.CM")
1524            else:
1525                message = f"Did not receive response from PGN {PGN['ECUID', [32]].id}"
1526                logger.write_failure_to_html_report(message)
1527                pytest.fail(message)
1528
1529            if len(info) > 0:
1530                ecu = {
1531                    "part_number": info[0],
1532                    "serial_number": info[1],
1533                    "location_name": info[2],
1534                    "manufacturer": info[3],
1535                    "classification": info[4],
1536                }
1537                logger.write_result_to_html_report("<span style='font-weight: bold'>ECUID Information </span>")
1538                for key, value in ecu.items():
1539                    logger.write_result_to_html_report(f"{key.strip().replace('_', ' ').title()}: {value}")
1540            else:
1541                logger.write_failure_to_html_report("Could not get ECU information")
1542                pytest.fail("Could not get ECU information")

Gets information about the physical ECU and its hardware

@staticmethod
def bytes_to_ascii(bs: list[float]) -> list[str]:
1439    @staticmethod
1440    def bytes_to_ascii(bs: list[float]) -> list[str]:
1441        """Converts bytes to ASCII string"""
1442        s: str = ""
1443        for b in bs:
1444            h = re.sub(r"^[^0-9a-fA-F]+$", "", f"{b:x}")
1445            try:
1446                ba = bytearray.fromhex(h)[::-1]
1447                s += ba.decode("utf-8", "ignore")
1448                s = re.sub(r"[^\x20-\x7E]", "", s)
1449            except ValueError:
1450                # NOTE: This will ignore any invalid packets (from BrenTronics)
1451                logger.write_warning_to_report(f"Skipping invalid hex: {b:x}")
1452        return list(filter(None, s.split("*")))

Converts bytes to ASCII string

def test_ecu_information(self) -> None:
1454    def test_ecu_information(self) -> None:
1455        """
1456        | Description          | Get information from ECUID response                              |
1457        | :------------------- | :--------------------------------------------------------------- |
1458        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#395                                 |
1459        | Instructions         | 1. Request ECUID data                                       </br>\
1460                                 2. Validate data is within specifications                   </br>\
1461                                 3. Log response                                                  |
1462        | Estimated Duration   | 10 seconds                                                       |
1463        """
1464
1465        info = []
1466        with CANBus(BATTERY_CHANNEL) as bus:
1467            ecu_request = CANFrame(pgn=PGN["Request"], data=[PGN["ECUID"].id])
1468            if tp_cm_frame := bus.process_call(ecu_request):
1469                if tp_cm_frame is not None:
1470                    if tp_cm_frame.pgn.id == PGN["TP.CM", [32]].id:
1471                        data = []
1472                        cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0])
1473                        if data_frame := bus.process_call(cts_request):
1474                            if data_frame is not None:
1475                                if data_frame.pgn.id == PGN["TP.DT"].id:
1476                                    packet_count = int(tp_cm_frame.data[2])
1477                                    # NOTE: This will ignore the invalid header packet from BrenTronics
1478                                    if BATTERY_INFO.manufacturer_code != ManufacturerID.BRENTRONICS:
1479                                        data.append(data_frame.data[1])
1480                                        packet_count -= 1
1481                                    for _ in range(packet_count):
1482                                        data_message = bus.read_input()
1483                                        if data_message is not None:
1484                                            frame = CANFrame.decode(data_message.arbitration_id, data_message.data)
1485                                            if frame.pgn.id == PGN["TP.DT"].id:
1486                                                data.append(frame.data[1])
1487                                            else:
1488                                                Errors.unexpected_packet("TP.DT", frame)
1489                                                break
1490                                        else:
1491                                            Errors.no_packet("TP.DT")
1492                                            break
1493                                    info = self.bytes_to_ascii(data)
1494                                    eom_request = CANFrame(
1495                                        pgn=PGN["TP.CM"],
1496                                        data=[17, tp_cm_frame.data[1], packet_count, 0xFF, tp_cm_frame.data[-1]],
1497                                    )
1498                                    if eom_frame := bus.process_call(eom_request):
1499                                        if eom_frame is not None:
1500                                            if eom_frame.pgn.id == PGN["DM15"].id:
1501                                                if eom_frame.data[2] == 4:  # Operation Completed
1502                                                    logger.write_info_to_report("ECUID data transfer successful")
1503                                                else:
1504                                                    logger.write_warning_to_html_report("Unsuccessful EOM response")
1505                                            else:
1506                                                Errors.unexpected_packet("DM15", eom_frame)
1507                                        else:
1508                                            Errors.no_packet("DM15")
1509                                    else:
1510                                        # timeout
1511                                        logger.write_warning_to_report("No response after sending EOM (DM15)")
1512                                else:
1513                                    Errors.unexpected_packet("TP.DT", data_frame)
1514                            else:
1515                                Errors.no_packet("TP.DT")
1516                        else:
1517                            message = f"Did not receive response from PGN {PGN['TP.CM', [32]].id} (TP.CM)"
1518                            logger.write_failure_to_html_report(message)
1519                            pytest.fail(message)
1520                    else:
1521                        Errors.unexpected_packet("TP.CM", tp_cm_frame)
1522                else:
1523                    Errors.no_packet("TP.CM")
1524            else:
1525                message = f"Did not receive response from PGN {PGN['ECUID', [32]].id}"
1526                logger.write_failure_to_html_report(message)
1527                pytest.fail(message)
1528
1529            if len(info) > 0:
1530                ecu = {
1531                    "part_number": info[0],
1532                    "serial_number": info[1],
1533                    "location_name": info[2],
1534                    "manufacturer": info[3],
1535                    "classification": info[4],
1536                }
1537                logger.write_result_to_html_report("<span style='font-weight: bold'>ECUID Information </span>")
1538                for key, value in ecu.items():
1539                    logger.write_result_to_html_report(f"{key.strip().replace('_', ' ').title()}: {value}")
1540            else:
1541                logger.write_failure_to_html_report("Could not get ECU information")
1542                pytest.fail("Could not get ECU information")
Description Get information from ECUID response
GitHub Issue turnaroundfactor/BMS-HW-Test#395
Instructions 1. Request ECUID data
2. Validate data is within specifications
3. Log response
Estimated Duration 10 seconds
def memory_test(mode: Modes) -> list[int]:
1545def memory_test(mode: Modes) -> list[int]:
1546    """Memory tester helper"""
1547    found_addresses = []
1548    with CANBus(BATTERY_CHANNEL) as bus:
1549        read_request_frame = CANFrame(pgn=PGN["DM14"], data=[1, 1, 1, 0, 0, 0, 0, 0])
1550        read_request_frame.data[2] = mode
1551        addresses = [0, 1107296256, 2147483648, 4294966271]  # 0x0, 0x42000000, 0x80000000, 0xfffffbff
1552        for low_address in addresses:
1553            found = 0
1554            high_address = low_address + (1024 if FULL_MEMORY_TESTS else 16)
1555            for i in range(low_address, high_address):
1556                read_request_frame.data[5] = low_address
1557                if response_frame := bus.process_call(read_request_frame, timeout=1):  # NOTE: SAFT times out
1558                    if response_frame is not None:
1559                        logger.write_info_to_report(
1560                            f"Address {i} responded with PGN {response_frame.pgn.id} "
1561                            f"({response_frame.pgn.short_name}) - status is: "
1562                            f"{spn_types.dm15_status(response_frame.data[3])}"
1563                        )
1564                        if response_frame.pgn.id == PGN["DM15"].id:
1565                            if response_frame.data[5] != 258:  # Invalid Length
1566                                if response_frame.data[3] == 0:
1567                                    found_addresses.append(i)
1568                                    found += 1
1569                        else:
1570                            Errors.unexpected_packet("DM15", response_frame)
1571                            break
1572                    else:
1573                        Errors.no_packet("DM15")
1574                        break
1575                else:
1576                    # timeout
1577                    pass
1578            verb = ""
1579            match mode:
1580                case Modes.READ:
1581                    verb = "readable"
1582                case Modes.WRITE:
1583                    verb = "writable"
1584                case Modes.ERASE:
1585                    verb = "erasable"
1586                case Modes.BOOT:
1587                    verb = "boot load"
1588            message = (
1589                f"Found {found} {verb} successful address(es) in memory ranges"
1590                f" {hex(low_address)}-{hex(high_address)}"
1591            )
1592            logger.write_result_to_html_report(message)
1593
1594    if len(found_addresses) > 0:
1595        logger.write_result_to_html_report(
1596            f"Found {len(found_addresses)} {verb} memory address(es) out of "
1597            f"{4096 if FULL_MEMORY_TESTS else 64} possible addresses"
1598        )
1599    else:
1600        message = f"Found 0 {verb} memory addresses out of {4096 if FULL_MEMORY_TESTS else 64} possible addresses"
1601        logger.write_warning_to_html_report(message)
1602    return found_addresses

Memory tester helper

class TestMemoryRead:
1605class TestMemoryRead:
1606    """This will test the read capability of the memory"""
1607
1608    def test_read(self) -> None:
1609        """
1610        | Description          | Try to read from different memory locations                      |
1611        | :------------------- | :--------------------------------------------------------------- |
1612        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#379                            </br>\
1613                                 turnaroundfactor/BMS-HW-Test#380                                 |
1614        | Instructions         | 1. Request memory read                                      </br>\
1615                                 2. Log any successful addresses                                  |
1616        | Estimated Duration   | 1 minute                                                         |
1617        """
1618        memory_test(Modes.READ)

This will test the read capability of the memory

def test_read(self) -> None:
1608    def test_read(self) -> None:
1609        """
1610        | Description          | Try to read from different memory locations                      |
1611        | :------------------- | :--------------------------------------------------------------- |
1612        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#379                            </br>\
1613                                 turnaroundfactor/BMS-HW-Test#380                                 |
1614        | Instructions         | 1. Request memory read                                      </br>\
1615                                 2. Log any successful addresses                                  |
1616        | Estimated Duration   | 1 minute                                                         |
1617        """
1618        memory_test(Modes.READ)
Description Try to read from different memory locations
GitHub Issue turnaroundfactor/BMS-HW-Test#379
turnaroundfactor/BMS-HW-Test#380
Instructions 1. Request memory read
2. Log any successful addresses
Estimated Duration 1 minute
class TestMemoryWrite:
1621class TestMemoryWrite:
1622    """This will test the write capability of the memory"""
1623
1624    def test_write(self) -> None:
1625        """
1626        | Description          | Try to write to different memory locations                       |
1627        | :------------------- | :--------------------------------------------------------------- |
1628        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#379                            </br>\
1629                                 turnaroundfactor/BMS-HW-Test#380                                 |
1630        | Instructions         | 1. Request memory write                                     </br>\
1631                                 2. Log any successful addresses                                  |
1632        | Estimated Duration   | 1 minute                                                         |
1633        """
1634        memory_test(Modes.WRITE)

This will test the write capability of the memory

def test_write(self) -> None:
1624    def test_write(self) -> None:
1625        """
1626        | Description          | Try to write to different memory locations                       |
1627        | :------------------- | :--------------------------------------------------------------- |
1628        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#379                            </br>\
1629                                 turnaroundfactor/BMS-HW-Test#380                                 |
1630        | Instructions         | 1. Request memory write                                     </br>\
1631                                 2. Log any successful addresses                                  |
1632        | Estimated Duration   | 1 minute                                                         |
1633        """
1634        memory_test(Modes.WRITE)
Description Try to write to different memory locations
GitHub Issue turnaroundfactor/BMS-HW-Test#379
turnaroundfactor/BMS-HW-Test#380
Instructions 1. Request memory write
2. Log any successful addresses
Estimated Duration 1 minute
class TestMemoryErase:
1637class TestMemoryErase:
1638    """This will test the erase capability of the memory"""
1639
1640    def test_erase(self) -> None:
1641        """
1642        | Description          | Try to erase different memory locations                          |
1643        | :------------------- | :--------------------------------------------------------------- |
1644        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#379                            </br>\
1645                                 turnaroundfactor/BMS-HW-Test#380                                 |
1646        | Instructions         | 1. Request memory erase                                     </br>\
1647                                 2. Log any successful addresses                                  |
1648        | Estimated Duration   | 1 minute                                                         |
1649        """
1650        memory_test(Modes.ERASE)

This will test the erase capability of the memory

def test_erase(self) -> None:
1640    def test_erase(self) -> None:
1641        """
1642        | Description          | Try to erase different memory locations                          |
1643        | :------------------- | :--------------------------------------------------------------- |
1644        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#379                            </br>\
1645                                 turnaroundfactor/BMS-HW-Test#380                                 |
1646        | Instructions         | 1. Request memory erase                                     </br>\
1647                                 2. Log any successful addresses                                  |
1648        | Estimated Duration   | 1 minute                                                         |
1649        """
1650        memory_test(Modes.ERASE)
Description Try to erase different memory locations
GitHub Issue turnaroundfactor/BMS-HW-Test#379
turnaroundfactor/BMS-HW-Test#380
Instructions 1. Request memory erase
2. Log any successful addresses
Estimated Duration 1 minute
class TestMemoryBootLoad:
1653class TestMemoryBootLoad:
1654    """This will test the boot load capability of the memory"""
1655
1656    def test_boot(self) -> None:
1657        """
1658        | Description          | Try to boot load from different memory locations                 |
1659        | :------------------- | :--------------------------------------------------------------- |
1660        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#381                            </br>\
1661                                 turnaroundfactor/BMS-HW-Test#382                                 |
1662        | Instructions         | 1. Request memory boot load                                 </br>\
1663                                 2. Log any successful addresses                                  |
1664        | Estimated Duration   | 1 minute                                                         |
1665        """
1666        memory_test(Modes.BOOT)

This will test the boot load capability of the memory

def test_boot(self) -> None:
1656    def test_boot(self) -> None:
1657        """
1658        | Description          | Try to boot load from different memory locations                 |
1659        | :------------------- | :--------------------------------------------------------------- |
1660        | GitHub Issue         | turnaroundfactor/BMS-HW-Test#381                            </br>\
1661                                 turnaroundfactor/BMS-HW-Test#382                                 |
1662        | Instructions         | 1. Request memory boot load                                 </br>\
1663                                 2. Log any successful addresses                                  |
1664        | Estimated Duration   | 1 minute                                                         |
1665        """
1666        memory_test(Modes.BOOT)
Description Try to boot load from different memory locations
GitHub Issue turnaroundfactor/BMS-HW-Test#381
turnaroundfactor/BMS-HW-Test#382
Instructions 1. Request memory boot load
2. Log any successful addresses
Estimated Duration 1 minute