"""Implement CAN-based bootloader communication.
This module defines the CAN-based implementation of the bootloader interface
used to communicate with the bootloader on a higher abstraction level.
It builds on the low-level CAN basics to send commands, transfer program
metadata and data, and receive acknowledgements from the bootloader.
"""
import time
from enum import Enum
from pathlib import Path
from typing import cast
from can import BusABC
from ..helpers.logger import logger
from ..helpers.misc import APP_DBC_FILE, BOOTLOADER_DBC_FILE
from .bootloader import BootloaderInterface
from .bootloader_can_basics import BootloaderCanBasics
from .bootloader_can_messages import (
AcknowledgeMessage,
AcknowledgeMessageType,
BootFsmState,
BootloaderAction,
CanFsmState,
StatusCode,
YesNoFlag,
)
class BootloaderInterfaceCan(BootloaderInterface):
"""Implement CAN-specific bootloader interface for higher-level operations.
This class provides all CAN-related functions required to interact with
the bootloader. It wraps low-level CAN messaging to send commands,
wait for and receive specific messages on the CAN bus, and control the
transfer and validation of application data.
"""
def __init__(
self,
can_bus: BusABC,
app_dbc: Path = APP_DBC_FILE,
bootloader_dbc: Path = BOOTLOADER_DBC_FILE,
) -> None:
"""Init function.
Args:
can_bus: CAN bus object
"""
self.app_dbc = app_dbc
self.can = BootloaderCanBasics(can_bus=can_bus, dbc_file=bootloader_dbc)
def send_crc(
self, crc_8_bytes: int, is_crc_of_vector_table: bool = False
) -> tuple[bool, bool]:
"""Send CRC signature to the bootloader and evaluate its acknowledgement.
This method transmits an 8-byte CRC value to the bootloader and waits
for the corresponding acknowledgement message. It can handle CRCs for
both regular data sectors and the vector table, depending on the
`is_crc_of_vector_table` flag.
Args:
crc_8_bytes: CRC value as a 64-bit integer.
is_crc_of_vector_table: True if the CRC belongs to the vector
table, False if it belongs to a regular data sector.
Returns:
A tuple (is_ack_received, is_crc_valid) where:
- is_ack_received is True if an ACK message has been received,
False otherwise.
- is_crc_valid is True if the ACK indicates a valid CRC
(YesNoFlag == "Yes"), False otherwise.
"""
self.can.send_crc_to_bootloader(crc_8_bytes)
if is_crc_of_vector_table:
ack_crc_msg = self.can.wait_can_ack_msg(
AcknowledgeMessage.ReceivedCrcOfVectorTable,
timeout_total=120,
timeout_ack=30,
)
else:
ack_crc_msg = self.can.wait_can_ack_msg(
AcknowledgeMessage.Received8BytesCrc, timeout_total=120, timeout_ack=30
)
if not ack_crc_msg:
logger.error("Cannot receive ACK message.")
return False, False
is_crc_valid = ack_crc_msg["Response"] == "Yes"
if is_crc_valid:
logger.debug("Successfully send 8 bytes crc to bootloader.")
else:
logger.debug("The validation for this sector failed.")
return True, is_crc_valid
def send_program_info(
self,
len_of_program_in_bytes: int,
num_of_transfer_loops: int,
) -> bool:
"""Send program info to bootloader and check its states.
Args:
len_of_program_in_bytes: the length of the program in bytes.
num_of_transfer_loops: the number of total transfer loops.
Returns:
True if bootloader has received info, False otherwise.
"""
logger.debug("Start sending program info to bootloader ...")
self.can.send_transfer_program_info_to_bootloader(
len_of_program_in_bytes=len_of_program_in_bytes,
num_of_transfer_loops=num_of_transfer_loops,
)
msg = self.can.wait_can_ack_msg(AcknowledgeMessage.ReceivedProgramInfo)
if not msg:
logger.error(
"Cannot receive the ACK message containing 'ReceivedAndProcessed'."
)
return False
if msg["Response"] != YesNoFlag.Yes.name:
logger.error(
"The program info is not valid, check if the program length "
"is larger than the maximum and/or if the "
"num_of_transfer_loops is correct."
)
return False
can_fsm_state, _ = self.get_bootloader_state()
if can_fsm_state != CanFsmState.CanFsmStateWaitForDataLoops.name:
logger.error(
"Cannot change the state of CAN module to wait for data loops."
)
return False
logger.debug("Successfully start the transfer process on bootloader.")
return True
def send_loop_number_to_bootloader(self, num_of_loop: int) -> None:
"""Send the current loop number of the to-be-transferred data to
bootloader.
Args:
num_of_loop: the current loop number (starting from 1).
"""
return self.can.send_loop_number_to_bootloader(num_of_loop=num_of_loop)
def send_data_to_bootloader(self, data_8_bytes: int) -> None:
"""Send data in 8 bytes to bootloader.
Args:
data_8_bytes: data in 8 bytes.
"""
time.sleep(0.0001)
self.can.send_data_to_bootloader(data_8_bytes=data_8_bytes)
def wait_can_ack_msg(
self,
acknowledge_msg: Enum,
timeout_total: float = 1.0,
timeout_ack: float = 0.3,
processed_level: Enum = StatusCode.ReceivedAndProcessed,
) -> AcknowledgeMessageType | None:
"""Wait for a certain ACK message.
Args:
acknowledge_msg: the type of ACK message.
timeout_total: total time to wait.
timeout_ack: time to wait for a single message.
processed_level: the ACK message needs to have at least this
processed_level.
Returns:
acknowledged message from bootloader
"""
return self.can.wait_can_ack_msg(
acknowledge_msg=acknowledge_msg,
timeout_total=timeout_total,
timeout_ack=timeout_ack,
processed_level=processed_level,
)
def start_transfer(self) -> bool:
"""Inform bootloader to start the transfer program process.
Returns:
True if the bootloader has successfully started the process, False
otherwise.
"""
logger.debug(
"Send request to start CAN communication to transfer the program..."
)
self.can.send_request_to_bootloader(BootloaderAction.CmdToTransferProgram)
if not self.can.wait_can_ack_msg(
AcknowledgeMessage.ReceivedCmdToTransferProgram
):
logger.error(
"Cannot receive the ACK message to start transferring the program."
)
return False
can_fsm_state, _ = self.get_bootloader_state()
if can_fsm_state != CanFsmState.CanFsmStateWaitForInfo.name:
logger.error(
"Cannot set the CAN FSM state of the bootloader to %s.",
CanFsmState.CanFsmStateWaitForInfo.name,
)
return False
logger.debug("Successfully started the transfer process on the bootloader.")
return True
def reset_bootloader(
self, time_to_wait: float = 15.0, num_of_repeat: int = 5
) -> bool:
"""Reset bootloader.
Args:
time_to_wait: time to wait for bootloader to reset itself.
Returns:
True if the bootloader has been successfully reset, False
otherwise.
"""
logger.debug("Sending request to bootloader to reset boot process ...")
self.can.send_request_to_bootloader(BootloaderAction.CmdToResetBootProcess)
if not self.can.wait_can_ack_msg(
AcknowledgeMessage.ReceivedCmdToResetBootProcess,
processed_level=StatusCode.ReceivedAndInProcessing,
timeout_total=10,
timeout_ack=1,
):
logger.error("Cannot receive ACK message to reset boot process.")
return False
time.sleep(time_to_wait)
for _ in range(num_of_repeat):
can_fsm_state, boot_fsm_state = self.get_bootloader_state()
if can_fsm_state:
break
time.sleep(1)
logger.debug(
"can_fsm_state: %s, boot_fsm_state: %s", can_fsm_state, boot_fsm_state
)
current_num_of_loops = self.get_current_num_of_loops()
if (
(can_fsm_state != CanFsmState.CanFsmStateNoCommunication.name)
or (boot_fsm_state != BootFsmState.BootFsmStateWait.name)
or (current_num_of_loops != 0)
):
logger.error("Bootloader cannot be successfully reset.")
return False
logger.debug("Successfully resetted bootloader.")
return True
def run_app_on_bootloader(self) -> bool:
"""Run the BMS application on the BMS-Master (i.e., jump from the
bootloader into the application via BMS state request.)
Returns:
True if the application has been started successfully,
False otherwise.
"""
logger.info("Sending request to run application...")
self.can.send_request_to_bootloader(BootloaderAction.CmdToRunProgram)
if not self.can.wait_can_ack_msg(
AcknowledgeMessage.ReceivedCmdToRunProgram,
processed_level=StatusCode.ReceivedAndInProcessing,
):
logger.error("Did not receive ACK message to run application.")
return False
msg_ack = self.can.wait_can_ack_msg(
AcknowledgeMessage.ReceivedCmdToRunProgram,
timeout_total=5,
timeout_ack=0.3,
processed_level=StatusCode.ReceivedAndProcessed,
)
if not msg_ack:
logger.error(
"Did not receive ACK message with program availability message.\n"
"There might be no application program available on the BMS-Master."
)
return False
if msg_ack["Response"] != YesNoFlag.Yes.name:
logger.error("There is no valid program available on the BMS.")
return False
logger.info(
"There is a validated program available on the BMS-Master.\n"
"Jumping into application..."
)
time.sleep(2)
logger.info(
"Successfully informed bootloader to run the BMS application.\n"
"Checking if the application is running..."
)
bms_state = self.get_foxbms_state()
if not bms_state:
logger.error("Could not jump into the application.")
return False
logger.info(
"Successfully jumped into the application. foxBMS status: %s ",
bms_state,
)
return True
def get_foxbms_state(self) -> str | None:
"""Get the current state of foxBMS application if it is running.
Returns:
None if no application is running, otherwise the current bms state.
"""
msg = None
bms_state = None
msg = self.can.wait_can_message(
arbitration_id=0x220,
dbc_file=self.app_dbc,
timeout_total=0.05,
timeout_bus_recv=0.001,
)
if msg:
bms_state = cast(dict, msg)["BmsState"]
return bms_state
def get_bootloader_state(self) -> tuple[str | None, str | None]:
"""Ask bootloader for its current state.
Returns:
CanFsmState, BootFsmState.
"""
logger.debug("Sending request to bootloader to get bootloader states...")
self.can.send_request_to_bootloader(BootloaderAction.CmdToGetBootloaderInfo)
msg_bootloader_fsm_states = self.can.wait_bootloader_state_msg()
if msg_bootloader_fsm_states:
can_fsm_state = msg_bootloader_fsm_states["CanFsmState"]
boot_fsm_state = msg_bootloader_fsm_states["BootFsmState"]
logger.debug(
"can_fsm_state: %s, boot_fsm_state: %s", can_fsm_state, boot_fsm_state
)
return can_fsm_state, boot_fsm_state
logger.error("Can not get the state of bootloader.")
return None, None
def get_current_num_of_loops(self) -> int | None:
"""Ask the bootloader to get its current number of data transfer loops.
Returns:
The current number of data transfer loops on bootloader.
"""
logger.debug("Sending request to bootloader to ask for number of loops...")
self.can.send_request_to_bootloader(BootloaderAction.CmdToGetDataTransferInfo)
msg_data_transfer_info = self.can.wait_data_transfer_info_msg()
if msg_data_transfer_info:
current_num_of_loops = msg_data_transfer_info["CurrentLoopNumber"]
logger.debug("current_num_of_loops: %d", current_num_of_loops)
return current_num_of_loops
logger.error(
"Can not get the current number of data transfer loops of bootloader."
)
return None
def get_bootloader_version_num(self) -> tuple[int | None, int | None, int | None]:
"""Get the bootloader's version number.
Returns:
Current version number (MajorVersionNumber, MinorVersionNumber,
PatchVersionNumber) of the bootloader.
"""
logger.debug("Sending request to bootloader to ask for the version number...")
self.can.send_request_to_bootloader(BootloaderAction.CmdToGetVersionInfo)
msg_bootloader_version_info = self.can.wait_bootloader_version_info_msg()
if msg_bootloader_version_info:
major_version_number = msg_bootloader_version_info["MajorVersionNumber"]
minor_version_number = msg_bootloader_version_info["MinorVersionNumber"]
patch_version_number = msg_bootloader_version_info["PatchVersionNumber"]
logger.debug(
"The version number of bootloader: %d.%d.%d",
major_version_number,
minor_version_number,
patch_version_number,
)
return major_version_number, minor_version_number, patch_version_number
logger.error(
"Can not get the current number of data transfer loops of bootloader."
)
return None, None, None