# SPDX-FileCopyrightText: © 2023 Tenstorrent Inc.
# SPDX-License-Identifier: Apache-2.0

"""
This is the backend of tt-smi.
    - Keeps track of chip objects and tasks related to fetching device info and telemetry
    - Sanitizes input for frontend
"""

import os
import re
import sys
import time
import datetime
from tt_smi import log
from pathlib import Path
from rich.table import Table
from tt_smi import constants
from rich import get_console
from rich.syntax import Syntax
from typing import Any, Dict, List, Optional, Union
from rich.progress import track
from tt_tools_common.ui_common.themes import CMD_LINE_COLOR
from pyluwen import PciChip
from tt_smi.tt_smi_utils import (
    LOG_FOLDER,
    hex_to_date,
    hex_to_semver_eth,
    hex_to_semver_m3_fw,
    hex_to_semver_eth_wh,
    get_board_type,
    convert_signed_16_16_to_float,
    dict_from_public_attrs,
    get_host_software_versions,
    get_fw_bundle_version
)
from tt_umd import (
    TTDevice,
    wormhole,
    TelemetryTag,
    ClusterDescriptor,
    SmBusArcTelemetryReader,
    ARCH,
)
from tt_tools_common.utils_common.system_utils import (
    get_host_info,
)
from tt_tools_common.utils_common.tools_utils import init_logging

class TTSMIBackend:
    """
    TT-SMI backend class that encompasses all chip objects on host.
    It handles all device related tasks like fetching device info, telemetry and toggling resets
    """

    def __init__(
        self,
        devices: Dict[int, Union[PciChip, TTDevice]] = None,
        umd_cluster_descriptor: Optional[ClusterDescriptor] = None,
        fully_init: bool = True,
        pretty_output: bool = True,
    ):
        self.devices = devices
        self.use_umd = umd_cluster_descriptor is not None
        if (self.use_umd):
            self.umd_cluster_descriptor = umd_cluster_descriptor
        self.pretty_output = pretty_output
        self.log: log.TTSMILog = log.TTSMILog(
            time=datetime.datetime.now(),
            host_info=get_host_info(),
            host_sw_vers=get_host_software_versions(),
            device_info=[
                log.TTSMIDeviceLog(
                    smbus_telem=log.SmbusTelem(),
                    board_info=log.BoardInfo(),
                    telemetry=log.Telemetry(),
                    firmwares=log.Firmwares(),
                    limits=log.Limits(),
                )
                for device in self.devices
            ],
        )
        self.smbus_telem_info = []
        self.firmware_infos = []
        self.device_infos = []
        self.device_telemetrys = []
        self.chip_limits = []
        self.pci_properties = []

        if fully_init:
            for i, _ in track(
                self.devices.items(),
                total=len(self.devices),
                description="Gathering Information",
                update_period=0.01,
                disable=not self.pretty_output,
            ):
                self.smbus_telem_info.append(self.get_smbus_board_info(i))
                self.firmware_infos.append(self.get_firmware_versions(i))
                self.pci_properties.append(self.get_pci_properties(i))
                self.device_infos.append(self.get_device_info(i))
                self.device_telemetrys.append(self.get_chip_telemetry(i))
                self.chip_limits.append(self.get_chip_limits(i))
    
    def is_blackhole(self, device_idx) -> bool:
        return (self.devices[device_idx].as_bh() if not self.use_umd
                else self.devices[device_idx].get_arch() == ARCH.BLACKHOLE)
    
    def is_wormhole(self, device_idx) -> bool:
        return (self.devices[device_idx].as_wh() if not self.use_umd
                else self.devices[device_idx].get_arch() == ARCH.WORMHOLE_B0)

    def is_grayskull(self, device_idx) -> bool:
        return (self.devices[device_idx].as_gs() if not self.use_umd
                else False)
    
    def get_pci_device_id(self, device_idx) -> str:
        if self.devices[device_idx].is_remote():
            return "N/A"
        return (self.devices[device_idx].get_pci_interface_id() if not self.use_umd
                else self.devices[device_idx].get_pci_device().get_device_num())
        
    def get_pci_bdf(self, device_idx) -> str:
        if self.devices[device_idx].is_remote():
            return "N/A"
        return (self.devices[device_idx].get_pci_bdf() if not self.use_umd
                else self.devices[device_idx].get_pci_device().get_device_info().pci_bdf)
    
    def get_device_name(self, device_idx):
        """Get device name from chip object"""
        if self.is_wormhole(device_idx):
            return "Wormhole"
        elif self.is_blackhole(device_idx):
            return "Blackhole"
        else:
            assert False, "Unknown chip name, FIX!"

    def save_logs_to_file(self, result_filename: str = ""):
        """Save log for smi snapshots"""
        time_now = datetime.datetime.now()
        date_string = time_now.strftime("%m-%d-%Y_%H:%M:%S")
        if not os.path.exists(LOG_FOLDER):
            init_logging(LOG_FOLDER)
        log_filename = f"{LOG_FOLDER}{date_string}_results.json"
        if result_filename:
            dir_path = os.path.dirname(os.path.realpath(result_filename))
            Path(dir_path).mkdir(parents=True, exist_ok=True)
            log_filename = result_filename

        clean_json = self.get_logs_json()
        with open(log_filename, "w") as f:
            f.write(clean_json)

        return log_filename

    def print_logs_to_stdout(self, pretty: bool = True):
        """Pretty-print (or just print) logs to stdout"""
        clean_json = self.get_logs_json()

        if not pretty:
            print(clean_json)
            return

        formatted = Syntax(clean_json, "json", background_color="default")
        console = get_console()
        console.print(formatted)

    def get_logs_json(self) -> str:
        """Get logs as JSON"""
        for i in self.devices:
            self.log.device_info[i].smbus_telem = self.smbus_telem_info[i]
            self.log.device_info[i].board_info = self.device_infos[i]
            # Add L/R for nb300 to separate local and remote asics
            if self.is_wormhole(i):
                board_type = self.device_infos[i]["board_type"]
                suffix = " R" if self.devices[i].is_remote() else " L"
                board_type = board_type + suffix
                self.log.device_info[i].board_info["board_type"] = board_type
            self.log.device_info[i].telemetry = self.device_telemetrys[i]
            self.log.device_info[i].firmwares = self.firmware_infos[i]
            self.log.device_info[i].limits = self.chip_limits[i]

        return self.log.get_clean_json_string()

    def print_all_available_devices_luwen(self):
        """Print all available boards on host (Luwen path). Shows both PCI BDF and PCI Dev ID. Doesn't show UMD Chip ID."""
        console = get_console()
        table_1 = Table(title="All available boards on host:")
        table_1.add_column("PCI BDF")
        table_1.add_column("PCI Dev ID")
        table_1.add_column("Board Type")
        table_1.add_column("Device Series")
        table_1.add_column("Board Number")
        for i in self.devices:
            board_id = self.device_infos[i]["board_id"]
            board_type = self.device_infos[i]["board_type"]
            pci_dev_id = self.get_pci_device_id(i)
            if self.is_wormhole(i):
                suffix = " R" if self.devices[i].is_remote() else " L"
                board_type = board_type + suffix

            table_1.add_row(
                f"{self.get_pci_bdf(i)}",
                f"/dev/tenstorrent/{i}",
                f"{self.get_device_name(i)}",
                f"{board_type}",
                f"{board_id}",
            )
        console.print(table_1)
        table_2 = Table(title="Boards that can be reset:")
        table_2.add_column("PCI BDF")
        table_2.add_column("PCI Dev ID")
        table_2.add_column("Board Type")
        table_2.add_column("Device Series")
        table_2.add_column("Board Number")
        for i in self.devices:
            if (
                not self.devices[i].is_remote()
                and self.device_infos[i]["board_type"] != "wh_4u"
            ):
                board_id = self.device_infos[i]["board_id"]
                board_type = self.device_infos[i]["board_type"]
                pci_dev_id = self.get_pci_device_id(i)
                if self.is_wormhole(i):
                    suffix = " R" if self.devices[i].is_remote() else " L"
                    board_type = board_type + suffix
                table_2.add_row(
                    f"{self.get_pci_bdf(i)}",
                    f"/dev/tenstorrent/{i}",
                    f"{self.get_device_name(i)}",
                    f"{board_type}",
                    f"{board_id}",
                )
        console.print(table_2)

    def print_all_available_devices_umd(self):
        """Print all available boards on host (UMD path). Shows UMD Chip ID, PCI BDF, and PCI Dev ID."""
        if not self.use_umd:
            raise RuntimeError("print_all_available_devices_umd requires UMD backend (umd_cluster_descriptor set)")
        console = get_console()
        table_1 = Table(title="All available boards on host (UMD):")
        table_1.add_column("UMD Chip ID")
        table_1.add_column("PCI BDF")
        table_1.add_column("PCI Dev ID")
        table_1.add_column("Board Type")
        table_1.add_column("Device Series")
        table_1.add_column("Board Number")
        for i in self.devices:
            pci_dev_num = self.get_pci_device_id(i)
            board_id = self.device_infos[i]["board_id"]
            board_type = self.device_infos[i]["board_type"]
            if self.is_wormhole(i):
                suffix = " R" if self.devices[i].is_remote() else " L"
                board_type = board_type + suffix
            table_1.add_row(
                f"{i}",
                f"{self.get_pci_bdf(i)}",
                f"/dev/tenstorrent/{pci_dev_num}",
                f"{self.get_device_name(i)}",
                f"{board_type}",
                f"{board_id}",
            )
        console.print(table_1)
        table_2 = Table(title="Boards that can be reset (UMD):")
        table_2.add_column("UMD Chip ID")
        table_2.add_column("PCI BDF")
        table_2.add_column("PCI Dev ID")
        table_2.add_column("Board Type")
        table_2.add_column("Device Series")
        table_2.add_column("Board Number")
        for i in self.devices:
            if (
                not self.devices[i].is_remote()
                and self.device_infos[i]["board_type"] != "wh_4u"
            ):
                pci_dev_num = self.get_pci_device_id(i)
                board_id = self.device_infos[i]["board_id"]
                board_type = self.device_infos[i]["board_type"]
                if self.is_wormhole(i):
                    suffix = " R" if self.devices[i].is_remote() else " L"
                    board_type = board_type + suffix
                table_2.add_row(
                    f"{i}",
                    f"{self.get_pci_bdf(i)}",
                    f"/dev/tenstorrent/{pci_dev_num}",
                    f"{self.get_device_name(i)}",
                    f"{board_type}",
                    f"{board_id}",
                )
        console.print(table_2)

    def print_tray_and_device_mapping(self):
        """Print the mapping of trays to devices on the galaxy"""

        ubb_bus_ids = (
            constants.BH_UBB_BUS_IDS
            if self.devices[0].as_bh()
            else constants.WH_UBB_BUS_IDS
        )

        def _get_mapping_tray_to_device():
            bus_id_to_tray = {
                bus_id: tray_num for tray_num, bus_id in ubb_bus_ids.items()
            }
            tray_to_devices = {}
            for i, device_info in enumerate(self.device_infos):
                # Extract bus id from "domain:bus:device.function" format
                bus_id = int(device_info["bus_id"].split(":")[1], 16)
                tray_bus_id = bus_id & 0xF0
                if tray_bus_id in bus_id_to_tray:
                    tray_num = bus_id_to_tray[tray_bus_id]
                    tray_to_devices.setdefault(tray_num, []).append(i)
            return tray_to_devices

        tray_to_devices = _get_mapping_tray_to_device()

        console = get_console()
        table = Table(title="Mapping of trays to devices on the galaxy:")
        table.add_column("Tray Number")
        table.add_column("Tray Bus ID")
        table.add_column("/dev/tenstorrent/<id>")
        for tray_num in sorted(tray_to_devices):
            table.add_row(
                f"{tray_num}",
                f"0x{ubb_bus_ids[tray_num]:02x}",
                f"{','.join(map(str, tray_to_devices[tray_num]))}",
            )
        console.print(table)

    def get_smbus_board_info(self, board_num: int) -> Dict:
        """Update board info by reading SMBUS_TELEMETRY"""
        if self.use_umd:
            smbus_telem_dict = {}
            arch = self.devices[board_num].get_arch()
            if arch == ARCH.WORMHOLE_B0:
                telem_reader = SmBusArcTelemetryReader(self.devices[board_num])
                tag_collection = wormhole.TelemetryTag
            elif arch == ARCH.BLACKHOLE:
                telem_reader = self.devices[board_num].get_arc_telemetry_reader()
                tag_collection = TelemetryTag
            else:
                raise ValueError(f"Unknown arch for device {board_num}")
            
            for telem_key in tag_collection:
                telem_value = hex(telem_reader.read_entry(telem_key.value)) if telem_reader.is_entry_available(telem_key.value) else None
                smbus_telem_dict[telem_key.name] = telem_value
            return smbus_telem_dict
        
        pyluwen_chip = self.devices[board_num]
        if pyluwen_chip.as_bh():
            telem_struct = pyluwen_chip.as_bh().get_telemetry()
        elif pyluwen_chip.as_wh():
            telem_struct = pyluwen_chip.as_wh().get_telemetry()
        else:
            raise ValueError(f"Unknown chip type for device {board_num}")
        json_map = dict_from_public_attrs(telem_struct)
        smbus_telem_dict = dict.fromkeys(constants.SMBUS_TELEMETRY_LIST)

        for key, value in json_map.items():
            if value is not None:
                value = hex(value)
            smbus_telem_dict[key.upper()] = value
        return smbus_telem_dict

    def update_telem(self):
        """Update telemetry in a given interval"""
        for i in self.devices:
            self.smbus_telem_info[i] = self.get_smbus_board_info(i)
            self.device_telemetrys[i] = self.get_chip_telemetry(i)

    def get_board_id(self, board_num) -> str:
        """Read board id from CSM or SPI if FW is not loaded"""
        if "BOARD_ID" in self.smbus_telem_info[board_num] and self.smbus_telem_info[board_num]["BOARD_ID"]:
            board_id = int(self.smbus_telem_info[board_num]["BOARD_ID"], base=16)
            return f"{board_id:016x}"
        else:
            board_info_0 = int(self.smbus_telem_info[board_num]["BOARD_ID_LOW"], base=16)
            board_info_1 = int(self.smbus_telem_info[board_num]["BOARD_ID_HIGH"], base=16)

            if board_info_0 is None or board_info_1 is None:
                return "N/A"
            return f"{board_info_1:08x}{board_info_0:08x}"

    def get_dram_speed(self, board_num) -> int:
        """Read DRAM Frequency from CSM and alternatively from SPI if FW not loaded on chip"""
        if self.is_blackhole(board_num):
            if self.smbus_telem_info[board_num]["DDR_SPEED"] is None:
                return "N/A"
            dram_speed = int(self.smbus_telem_info[board_num]["DDR_SPEED"], 16)
            # check if its div by 1000 and then return num GHz else MHz
            if dram_speed % 1000 == 0:
                dram_speed = dram_speed // 1000
                return f"{dram_speed}G"
            else:
                return f"{dram_speed}"
        elif self.is_wormhole(board_num):
            if self.smbus_telem_info[board_num]["DDR_STATUS"] is not None:
                dram_speed_raw = (
                    int(self.smbus_telem_info[board_num]["DDR_STATUS"], 16) >> 24
                )
                if dram_speed_raw == 0:
                    return "16G"
                elif dram_speed_raw == 1:
                    return "14G"
                elif dram_speed_raw == 2:
                    return "12G"
                elif dram_speed_raw == 3:
                    return "10G"
                elif dram_speed_raw == 4:
                    return "8G"
                else:
                    return None
            return "N/A"
        else:
            return "N/A"

    def get_pci_properties(self, board_num):
        """Get the PCI link speed and link width details from sysfs files"""
        if self.devices[board_num].is_remote():
            return {prop: "N/A" for prop in constants.PCI_PROPERTIES}

        try:
            pcie_bdf = self.get_pci_bdf(board_num)
            pci_bus_path = os.path.realpath(f"/sys/bus/pci/devices/{pcie_bdf}")
        except:
            return {prop: "N/A" for prop in constants.PCI_PROPERTIES}

        def get_pcie_gen(link_speed: str) -> int:
            if link_speed == "32.0":
                return 5
            if link_speed == "16.0":
                return 4
            elif link_speed == "8.0":
                return 3
            elif link_speed == "5.0":
                return 2
            elif link_speed == "2.5":
                return 1
            else:
                assert False, f"Invalid link speed {link_speed}"

        properties = {}

        for prop in constants.PCI_PROPERTIES:
            try:
                with open(os.path.join(pci_bus_path, prop), "r", encoding="utf-8") as f:
                    output = f.readline().rstrip()
                    value = re.findall(r"\d+\.\d+|\d+", output)[0]
                    if prop == "current_link_speed" or prop == "max_link_speed":
                        value = get_pcie_gen(value)
            except Exception:
                value = "N/A"
            properties[prop] = value
        return properties

    def get_dram_training_status(self, board_num) -> bool:
        """
        Get DRAM Training Status
        True means it passed training, False means it failed or did not train at all
        """
        if self.is_wormhole(board_num):
            # DDR_STATUS field in WH:
            # (31:24) dram speed
            # (23:0) per channel dram status (6 channels)
            # DRAM_TRAINING_FAIL = 0x1
            # DRAM_TRAINING_PASS = 0x2
            if self.smbus_telem_info[board_num]["DDR_STATUS"] is None:
                return False
            dram_status = (
                int(self.smbus_telem_info[board_num]["DDR_STATUS"], 16)) & 0xFFFFFF
            if dram_status == 0x222222:
                return True
            return False
        elif self.is_blackhole(board_num):
            dram_status = int(self.smbus_telem_info[board_num]["DDR_STATUS"], 16)
            if int(get_fw_bundle_version(self.smbus_telem_info[board_num]), 16) >= 0x13070000:
                # After FW version 19.7.0.3 (hex 0x13070003), the dram status is a 16-bit field with the following layout:
                # DDR Status:
                # [0]  - Training complete GDDR 0
                # [1]  - Error GDDR 0
                # [2]  - Training complete GDDR 1
                # [3]  - Error GDDR 1
                # ...
                # [14] - Training complete GDDR 7
                # [15] - Error GDDR 7
                # [16] - BIST complete GDDR 0
                # [17] - BIST failed GDDR 0
                # [18] - BIST complete GDDR 1
                # [19] - BIST failed GDDR 1
                # ...
                # [30] - BIST complete GDDR 7
                # [31] - BIST failed GDDR 7
                if dram_status == 0x55555555:
                    return True
                return False
            else:
                # Pre-19.7.0.3 DDR Status in BH is a 16-bit field with the following layout:
                #  [0] - Training complete GDDR 0
                #  [1] - Error GDDR 0
                #  [2] - Training complete GDDR 1
                #  [3] - Error GDDR 1
                #  ...
                #  [14] - Training Complete GDDR 7
                #  [15] - Error GDDR 7
                # 0x5555 = 0b0101010101010101 means all 8 channels trained successfully
                if dram_status == 0x5555:
                    return True
                return False
        else:
            return False

    def get_device_info(self, board_num) -> dict:
        dev_info = {}
        for field in constants.DEV_INFO_LIST:
            if field == "bus_id":
                try:
                    dev_info[field] = self.get_pci_bdf(board_num)
                except:
                    dev_info[field] = "N/A"
            elif field == "board_type":
                if self.get_board_id(board_num) == "N/A":
                    dev_info[field] = "N/A"
                else:
                    dev_info[field] = get_board_type(self.get_board_id(board_num))
            elif field == "board_id":
                dev_info[field] = self.get_board_id(board_num)
            elif field == "coords":
                if self.is_wormhole(board_num):
                    if self.use_umd:
                        if board_num in self.umd_cluster_descriptor.get_chip_locations():
                            eth_coord = self.umd_cluster_descriptor.get_chip_locations()[board_num]
                            dev_info[
                                field
                            ] = f"({eth_coord.x}, {eth_coord.y}, {eth_coord.rack}, {eth_coord.shelf})"
                        else:
                            dev_info[field] = "(0, 0, 0, 0)"
                    else:
                        dev_info[
                            field
                        ] = f"({self.devices[board_num].as_wh().get_local_coord().shelf_x}, {self.devices[board_num].as_wh().get_local_coord().shelf_y}, {self.devices[board_num].as_wh().get_local_coord().rack_x}, {self.devices[board_num].as_wh().get_local_coord().rack_y})"
                else:
                    dev_info[field] = "N/A"
            elif field == "dram_status":
                dev_info[field] = self.get_dram_training_status(board_num)
            elif field == "dram_speed":
                dev_info[field] = self.get_dram_speed(board_num)
            elif field == "pcie_speed":
                dev_info[field] = self.pci_properties[board_num]["current_link_speed"]
            elif field == "pcie_width":
                dev_info[field] = self.pci_properties[board_num]["current_link_width"]

        return dev_info

    def get_bh_chip_telemetry(self, board_num) -> Dict:
        """Get telemetry data for bh chip. None if ARC FW not running"""
        current = (
            int(self.smbus_telem_info[board_num]["TDC"], 16) & 0xFFFF
            if self.smbus_telem_info[board_num]["TDC"] is not None
            else 0
        )
        if self.smbus_telem_info[board_num]["VCORE"] is not None:
            voltage = int(self.smbus_telem_info[board_num]["VCORE"], 16) / 1000
        else:
            voltage = 10000
        power = (
            int(self.smbus_telem_info[board_num]["TDP"], 16) & 0xFFFF
            if self.smbus_telem_info[board_num]["TDP"] is not None
            else 0
        )
        asic_temperature = (
            (
                convert_signed_16_16_to_float(
                    int(self.smbus_telem_info[board_num]["ASIC_TEMPERATURE"], 16)
                )
            )
            if self.smbus_telem_info[board_num]["ASIC_TEMPERATURE"] is not None
            else 0
        )
        aiclk = (
            int(self.smbus_telem_info[board_num]["AICLK"], 16) & 0xFFFF
            if self.smbus_telem_info[board_num]["AICLK"] is not None
            else 0
        )
        timer_heartbeat = int(self.smbus_telem_info[board_num]["TIMER_HEARTBEAT"], 16) // 6 # Watchdog heartbeat, ~2 per second
        fan_speed = (
            min(int(self.smbus_telem_info[board_num]["FAN_RPM"], 16), 5000) / 50 # RPM to percent conversion
            if self.smbus_telem_info[board_num]["FAN_RPM"] is not None
            else 0
        )

        chip_telemetry = {
            "voltage": f"{voltage:4.2f}",
            "current": f"{current:5.1f}",
            "power": f"{power:5.1f}",
            "aiclk": f"{aiclk:4.0f}",
            "asic_temperature": f"{asic_temperature:4.1f}",
            "fan_speed": f"{fan_speed:3.0f}",
            "heartbeat": f"{timer_heartbeat}",
        }
        return chip_telemetry

    def get_wh_chip_telemetry(self, board_num) -> Dict:
        """Get telemetry data for WH chip. None if ARC FW not running"""
        current = int(self.smbus_telem_info[board_num]["TDC"], 16) & 0xFFFF
        if self.smbus_telem_info[board_num]["VCORE"] is not None:
            voltage = int(self.smbus_telem_info[board_num]["VCORE"], 16) / 1000
        else:
            voltage = 10000
        power = int(self.smbus_telem_info[board_num]["TDP"], 16) & 0xFFFF
        asic_temperature = (
            int(self.smbus_telem_info[board_num]["ASIC_TEMPERATURE"], 16) & 0xFFFF
        ) / 16
        aiclk = int(self.smbus_telem_info[board_num]["AICLK"], 16) & 0xFFFF
        arc3_heartbeat = int(self.smbus_telem_info[board_num]["ARC3_HEALTH"], 16) // 5 # Watchdog heartbeat, ~2 per second
        if self.smbus_telem_info[board_num]["FAN_SPEED"] is not None:
            fan_speed = int(self.smbus_telem_info[board_num]["FAN_SPEED"], 16)
        else:
            fan_speed = 0

        chip_telemetry = {
            "voltage": f"{voltage:4.2f}",
            "current": f"{current:5.1f}",
            "power": f"{power:5.1f}",
            "aiclk": f"{aiclk:4.0f}",
            "asic_temperature": f"{asic_temperature:4.1f}",
            "fan_speed": f"{fan_speed:3.0f}",
            "heartbeat": f"{arc3_heartbeat}"
        }

        return chip_telemetry

    def get_chip_telemetry(self, board_num) -> Dict:
        """Return the correct chip telemetry for a given board"""
        if self.is_blackhole(board_num):
            return self.get_bh_chip_telemetry(board_num)
        elif self.is_wormhole(board_num):
            return self.get_wh_chip_telemetry(board_num)
        else:
            print(
                CMD_LINE_COLOR.RED,
                f"Could not fetch telemetry for board {e}: Unrecognized board type!",
                CMD_LINE_COLOR.ENDC,
            )
            return {}

    def get_chip_limits(self, board_num: int) -> Dict[str, str]:
        if self.is_blackhole(board_num):
            return self.get_bh_chip_limits(board_num)
        elif self.is_wormhole(board_num):
            return self.get_wh_chip_limits(board_num)
        else:
            print(
                CMD_LINE_COLOR.RED,
                f"Could not fetch chip limits for board {board_num}: Unrecognized board type!",
                CMD_LINE_COLOR.ENDC,
            )
            return {}

    def get_wh_chip_limits(self, board_num: int) -> Dict[str, str]:
        """Get chip limits from the CSM. None if ARC FW not running"""

        chip_limits = {}
        for field in constants.LIMITS:
            if field == "vdd_min":
                vdd_limits = self.smbus_telem_info[board_num].get("VDD_LIMITS")
                chip_limits[field] = f"{(int(vdd_limits, 16) & 0xFFFF) / 1000:4.2f}" if vdd_limits else 0
            elif field == "vdd_max":
                vdd_limits = self.smbus_telem_info[board_num].get("VDD_LIMITS")
                chip_limits[field] = f"{(int(vdd_limits, 16) >> 16) / 1000:4.2f}" if vdd_limits else 0
            elif field == "tdp_limit":
                tdp = self.smbus_telem_info[board_num].get("TDP")
                chip_limits[field] = f"{int(tdp, 16) >> 16:3.0f}" if tdp else 0
            elif field == "tdc_limit":
                tdc = self.smbus_telem_info[board_num].get("TDC")
                chip_limits[field] = f"{int(tdc, 16) >> 16:3.0f}" if tdc else 0
            elif field == "asic_fmax":
                aiclk = self.smbus_telem_info[board_num].get("AICLK")
                chip_limits[field] = f"{int(aiclk, 16) >> 16:4.0f}" if aiclk else 0
            elif field == "therm_trip_l1_limit":
                thm_limits = self.smbus_telem_info[board_num].get("THM_LIMITS")
                chip_limits[field] = f"{int(thm_limits, 16) >> 16:2.0f}" if thm_limits else 0
            elif field == "thm_limit":
                thm_limits = self.smbus_telem_info[board_num].get("THM_LIMITS")
                chip_limits[field] = f"{int(thm_limits, 16) & 0xFFFF:2.0f}" if thm_limits else 0
            else:
                chip_limits[field] = 0
        return chip_limits

    def get_bh_chip_limits(self, board_num: int) -> Dict[str, str]:
        """Get chip limits from BH telemetry. None if ARC FW not running"""

        chip_limits = {}
        for field in constants.LIMITS:
            if field == "vdd_min":
                vdd_limits = self.smbus_telem_info[board_num].get("VDD_LIMITS")
                chip_limits[field] = f"{(int(vdd_limits, 16) & 0xFFFF) / 1000:4.2f}" if vdd_limits else 0
            elif field == "vdd_max":
                vdd_limits = self.smbus_telem_info[board_num].get("VDD_LIMITS")
                chip_limits[field] = f"{(int(vdd_limits, 16) >> 16) / 1000:4.2f}" if vdd_limits else 0
            elif field == "tdp_limit":
                tdp_limit = self.smbus_telem_info[board_num].get("TDP_LIMIT_MAX")
                chip_limits[field] = f"{int(tdp_limit, 16):3.0f}" if tdp_limit else 0
            elif field == "tdc_limit":
                tdc_limit = self.smbus_telem_info[board_num].get("TDC_LIMIT_MAX")
                chip_limits[field] = f"{int(tdc_limit, 16):3.0f}" if tdc_limit else 0
            elif field == "asic_fmax":
                asic_fmax = self.smbus_telem_info[board_num].get("AICLK_LIMIT_MAX")
                chip_limits[field] = f"{int(asic_fmax, 16):4.0f}" if asic_fmax else 0
            elif field == "therm_trip_l1_limit":
                therm_trip_l1_limit = self.smbus_telem_info[board_num].get("THM_LIMIT_THROTTLE")
                chip_limits[field] = f"{int(therm_trip_l1_limit, 16):2.0f}" if therm_trip_l1_limit else 0
            elif field == "thm_limit":
                thm_limits = self.smbus_telem_info[board_num].get("THM_LIMITS")
                chip_limits[field] = f"{int(thm_limits, 16):2.0f}" if thm_limits else 0
            else:
                chip_limits[field] = 0
        return chip_limits

    def get_firmware_versions(self, board_num):
        """Translate the telem struct semver for gui"""
        fw_versions = {}
        for field in constants.FW_LIST_SNAPSHOT:
            if field == "cm_fw":
                if "ARC0_FW_VERSION" in self.smbus_telem_info[board_num]:
                    val = self.smbus_telem_info[board_num]["ARC0_FW_VERSION"]
                elif "CM_FW_VERSION" in self.smbus_telem_info[board_num]:
                    val = self.smbus_telem_info[board_num]["CM_FW_VERSION"]
                else:
                    val = "N/A"
                if val is None:
                    fw_versions[field] = "N/A"
                else:
                    fw_versions[field] = hex_to_semver_m3_fw(int(val, 16))

            elif field == "cm_fw_date":
                if "WH_FW_DATE" in self.smbus_telem_info[board_num]:
                    val = self.smbus_telem_info[board_num]["WH_FW_DATE"]
                if val is None:
                    fw_versions[field] = "N/A"
                else:
                    fw_versions[field] = hex_to_date(int(val, 16), include_time=False)

            elif field == "eth_fw":
                val = self.smbus_telem_info[board_num]["ETH_FW_VERSION"]
                if val is None:
                    fw_versions[field] = "N/A"
                elif self.is_wormhole(board_num):
                    fw_versions[field] = hex_to_semver_eth_wh(int(val, 16))
                else:
                    fw_versions[field] = hex_to_semver_eth(int(val, 16))
            elif field == "dm_bl_fw":
                if self.use_umd:
                    # The tag has different value for old WH telemetry and new telemetry.
                    if "M3_BL_FW_VERSION" in self.smbus_telem_info[board_num]:
                        val = self.smbus_telem_info[board_num]["M3_BL_FW_VERSION"]
                    if "DM_BL_FW_VERSION" in self.smbus_telem_info[board_num]:
                        val = self.smbus_telem_info[board_num]["DM_BL_FW_VERSION"]
                else:
                    val = self.smbus_telem_info[board_num]["M3_BL_FW_VERSION"]
                if val is None:
                    fw_versions[field] = "N/A"
                else:
                    fw_versions[field] = hex_to_semver_m3_fw(int(val, 16))
            elif field == "dm_app_fw":
                if self.use_umd:
                    # The tag has different value for WH and BH
                    if "M3_APP_FW_VERSION" in self.smbus_telem_info[board_num]:
                        val = self.smbus_telem_info[board_num]["M3_APP_FW_VERSION"]
                    if "DM_APP_FW_VERSION" in self.smbus_telem_info[board_num]:
                        val = self.smbus_telem_info[board_num]["DM_APP_FW_VERSION"]
                else:
                    val = self.smbus_telem_info[board_num]["M3_APP_FW_VERSION"]
                if val is None:
                    fw_versions[field] = "N/A"
                else:
                    fw_versions[field] = hex_to_semver_m3_fw(int(val, 16))
            elif field == "tt_flash_version":
                if "TT_FLASH_VERSION" in self.smbus_telem_info[board_num]:
                    val = self.smbus_telem_info[board_num]["TT_FLASH_VERSION"]
                if val is None:
                    fw_versions[field] = "N/A"
                # See below- Galaxy systems manually get an N/A tt_flash_version
                elif get_board_type(self.get_board_id(board_num)) == "wh_4u":
                    fw_versions[field] = "N/A"
                else:
                    fw_versions[field] = hex_to_semver_m3_fw(int(val, 16))
            elif field == "fw_bundle_version":
                val = get_fw_bundle_version(self.smbus_telem_info[board_num])
                if (
                    get_board_type(self.get_board_id(board_num)) == "wh_4u"
                    and val == "0xffffffff"
                ):
                    # WARNING: Dirty dirty hack!
                    # See Issue #72 https://github.com/tenstorrent/tt-smi/issues/72
                    # Due to a FW flashing bug, Galaxy systems do not have the FW_BUNDLE_VERSION
                    # field set. The value ends up in TT_FLASH_VERSION, so we need to go get it.
                    val = self.smbus_telem_info[board_num]["TT_FLASH_VERSION"]
                    fw_versions[field] = hex_to_semver_m3_fw(int(val, 16))
                if val is None:
                    fw_versions[field] = "N/A"
                else:
                    fw_versions[field] = hex_to_semver_m3_fw(int(val, 16))
        return fw_versions
