# I3C High Level Analyzer

from saleae.analyzers import AnalyzerFrame
from SecondLayerHLAs.SecondLayerAnalyzerHandler import SecondLayerAnalyzerHandler
from I3CDictionaries import I3CDictionaries
from I3CDictionaries import DecodingModes
from GenericDecoder import GenericDecoder
import logging
import sys


# SDR Decoder must subclass the Generic Decoder class.
class SDRDecoder(GenericDecoder):

    I3C_logger = logging.getLogger("I3C")

    ccc_dict = {}
    result_types = I3CDictionaries.result_types

    temp_frame = None

    _MAXWR_RATE = {0: "fSCL", 1: "8M", 2: "6M", 3: "4M", 4: "2M"}
    _MAXRD_RATE = {0: "fSCL", 1: "8M", 2: "6M", 3: "4M", 4: "2M", 7: "=MaxWr"}
    _MAXRD_TSCO = {0: "≤8ns", 1: "≤12ns", 2: "≤15ns", 3: "≤20ns", 7: "Private"}
    _GETCAPS_GA = {0: "None", 1: "1", 2: "2", 3: "3+"}
    _RSTACT_ACTIONS = {
        0x00: "NoReset",
        0x01: "I3CPeriphReset",
        0x02: "WholeTargetReset",
        0x03: "DebugAdapterReset",
        0x04: "VirtualTargetDetect",
    }
    _SETXTIME_SUB_CMD = {
        0x7F: "ST",       # Sync Tick
        0xBF: "DT",       # Delay Time
        0xDF: "Async0",   # Enter Async Mode 0 (Basic)
        0xEF: "Async1",   # Enter Async Mode 1 (Advanced)
        0xF7: "Async2",   # Enter Async Mode 2 (High-Precision Low-Power)
        0xFB: "Async3",   # Enter Async Mode 3 (High-Precision Triggerable)
        0xFD: "Trigger",  # Async Trigger (Mode 3 only)
        0x3F: "TPH",      # Procedure Repetition Time
        0x9F: "TU",       # Time Unit
        0x8F: "ODR",      # Output Data Rate
        0xFF: "Exit",     # Exit timing modes
    }

    def __init__(self, second_layer_HLA_option, i3c_version_option):
        """
        Initialize HLA.

        Settings can be accessed using the same name used above.
        """

        logging.basicConfig(level=logging.INFO, stream=sys.stdout)

        """
        Obtain an instance of the second layer handler and call the method get to
        obtain an instance of the second layer hla that the user selected for SDR
        """
        handler = SecondLayerAnalyzerHandler()
        self.second_layer_analyzer = handler.get(
            second_layer_HLA_option, DecodingModes.SDR
        )
        self.ccc_dict = I3CDictionaries.getCCCDict(i3c_version_option)
        self.second_layer_analyzer.update_result_types(self.result_types)
        self.second_layer_analyzer.update_ccc(self.ccc_dict)

        self.entdaa_devices = None
        self.entdaa_byte_count = 0
        self.entdaa_current_device = {}


    def processFrame(self, frame: AnalyzerFrame):
        """
        Process a frame from the input analyzer, and optionally return a single `AnalyzerFrame` or a list of `AnalyzerFrame`s.

        The type and data values in `frame` will depend on the input analyzer.
        """
        if frame.type == "START" or (
            (frame.type == "R-START") and self.temp_frame is None
        ):
            # Create a new frame, defaulting as a Message frame
            self.temp_frame = AnalyzerFrame(
                "Message", frame.start_time, frame.end_time, {"Data": "", "Length": 0}
            )
            self.entdaa_devices = None
            self.entdaa_byte_count = 0
            self.entdaa_current_device = {}

        elif frame.type == "STOP":
            # Save the finished frame, clear temporary frame
            new_frame = self.temp_frame
            self.temp_frame = None
            entdaa_snapshot = self.entdaa_devices
            self.entdaa_devices = None
            self.entdaa_byte_count = 0
            self.entdaa_current_device = {}

            # Evaluate if it's necessary to return new_frame
            if new_frame is not None:
                if new_frame.data["Length"] > 0:
                    # Reformat ENTDAA frame as structured device info
                    if entdaa_snapshot is not None:
                        new_frame.data["Length"] = len(entdaa_snapshot)
                        new_frame.data["Data"] = self._format_entdaa_devices(entdaa_snapshot)
                        new_frame.data["DataVerbose"] = self._format_entdaa_devices_verbose(entdaa_snapshot)
                    # There is accumulated data
                    new_frame.end_time = frame.end_time
                    self._decode_direct_ccc_payload(new_frame)
                    s = self.result_types[new_frame.type]["console"]

                    try:
                        try:
                            # This will generate an error if there is any incomplete data in the frame (for example, an BC header without address)
                            self.I3C_logger.info(s.format(**new_frame.data))
                        except:
                            new_frame = AnalyzerFrame(
                                "Incomplete Transaction",
                                new_frame.start_time,
                                new_frame.end_time,
                                {
                                    "Data": new_frame.data["Data"],
                                    "Length": new_frame.data["Length"],
                                },
                            )
                            s = self.result_types[new_frame.type]["console"]
                            self.I3C_logger.info(s.format(**new_frame.data))
                        finally:
                            new_frame.data["Length"] = str(new_frame.data["Length"])
                            new_frame.data.pop("DataVerbose", None)

                        return self.second_layer_analyzer.instruction_decoder(new_frame), DecodingModes.SDR

                    except:
                        self.I3C_logger.error("Invalid transaction")

        elif frame.type == "R-START":
            # R-START conditions can represent 4 different cases
            if (
                self.temp_frame.type == "I3C Transaction"
                or self.temp_frame.type == "I3C Direct CCC Transaction"
            ) and ("Address" not in self.temp_frame.data):
                # Continue accumulating data, as this doesn't represent the end of a transaction
                pass
            elif self.temp_frame.type == "I3C ENTDAA Transaction":
                # R-START is an internal delimiter in the ENTDAA multi-device sequence; keep accumulating
                pass
            elif self.temp_frame.type == "I3C Direct CCC Transaction":
                # This is a complete Direct CCC transaction, with its associated transaction.
                # Commit the frame and prepare a new frame with the same type and CCC command
                new_frame = self.temp_frame
                self.temp_frame = AnalyzerFrame(
                    "Message",
                    frame.start_time,
                    frame.end_time,
                    {"Data": ""
                    , "Length": 0},
                )

                new_frame.end_time = frame.end_time
                self._decode_direct_ccc_payload(new_frame)
                if "DataVerbose" in new_frame.data:
                    s = self.result_types.get(new_frame.type, {}).get("console", "")
                    if s:
                        try:
                            self.I3C_logger.info(s.format(**new_frame.data))
                        except:
                            pass
                new_frame.data["Length"] = str(new_frame.data["Length"])
                new_frame.data.pop("DataVerbose", None)

                return self.second_layer_analyzer.instruction_decoder(new_frame), DecodingModes.SDR

            elif self.temp_frame.type == "I3C IBI Transaction":
                new_frame = self.temp_frame
                self.temp_frame = None
                if new_frame.data["Length"] > 0:
                    # There is accumulated data
                    new_frame.end_time = frame.end_time
                    s = self.result_types[new_frame.type]["console"]
                    try:
                        try:
                            # This will generate an error if there is any incomplete data in the frame (for example, an BC header without address)
                            self.I3C_logger.info(s.format(**new_frame.data))
                        except:
                            new_frame = AnalyzerFrame(
                                "Incomplete Transaction",
                                new_frame.start_time,
                                new_frame.end_time,
                                {
                                    "Data": new_frame.data["Data"],
                                    "Length": new_frame.data["Length"],
                                },
                            )
                            s = self.result_types[new_frame.type]["console"]
                            self.I3C_logger.info(s.format(**new_frame.data))
                        finally:
                            new_frame.data["Length"] = str(new_frame.data["Length"])

                        return self.second_layer_analyzer.instruction_decoder(new_frame), DecodingModes.SDR

                    except:
                        self.I3C_logger.error("Invalid transaction")
            elif self.temp_frame.type == "I3C BC CCC Transaction":
                new_frame = self.temp_frame
                self.temp_frame = AnalyzerFrame(
                    "Message",
                    frame.start_time,
                    frame.end_time,
                    {"Data": "", "Length": 0},
                )
                if new_frame is not None:
                    if new_frame.data["Length"] > 0:
                        new_frame.end_time = frame.end_time
                        self._decode_direct_ccc_payload(new_frame)
                        if "DataVerbose" in new_frame.data:
                            s = self.result_types.get(new_frame.type, {}).get("console", "")
                            if s:
                                try:
                                    self.I3C_logger.info(s.format(**new_frame.data))
                                except:
                                    pass
                        new_frame.data["Length"] = str(new_frame.data["Length"])
                        new_frame.data.pop("DataVerbose", None)
                        return self.second_layer_analyzer.instruction_decoder(new_frame), DecodingModes.SDR
            else:
                # Any other case shall be considered as a STOP + START condition
                new_frame = self.temp_frame
                self.temp_frame = AnalyzerFrame(
                    "Message",
                    frame.start_time,
                    frame.end_time,
                    {"Data": "", "Length": 0},
                )
                # Evaluate if it's necessary to return new_frame
                if new_frame is not None:
                    if new_frame.data["Length"] > 0:
                        # There is accumulated data
                        new_frame.end_time = frame.end_time
                        s = self.result_types[new_frame.type]["console"]
                        try:
                            try:
                                # This will generate an error if there is any incomplete data in the frame (for example, an BC header without address)
                                self.I3C_logger.info(s.format(**new_frame.data))
                            except:
                                new_frame = AnalyzerFrame(
                                    "Incomplete Transaction",
                                    new_frame.start_time,
                                    new_frame.end_time,
                                    {
                                        "Data": new_frame.data["Data"],
                                        "Length": new_frame.data["Length"],
                                    },
                                )
                                s = self.result_types[new_frame.type]["console"]
                                self.I3C_logger.info(s.format(**new_frame.data))
                            finally:
                                new_frame.data["Length"] = str(new_frame.data["Length"])

                            return self.second_layer_analyzer.instruction_decoder(new_frame), DecodingModes.SDR

                        except:
                            self.I3C_logger.error("Invalid transaction ")

        elif frame.type == "HDR EXIT":
            # Clear temporary frame
            self.temp_frame = None

            # Return a new frame
            new_frame = AnalyzerFrame(
                "I3C HDR Pattern",
                frame.start_time,
                frame.end_time,
                {"Info": frame.data["Info"]},
            )
            s = self.result_types[new_frame.type]["console"]
            try:
                try:
                    # This will generate an error if there is any incomplete data in the frame (for example, an BC header without address)
                    self.I3C_logger.info(s.format(**new_frame.data))
                except:
                    new_frame = AnalyzerFrame(
                        "Incomplete Transaction",
                        new_frame.start_time,
                        new_frame.end_time,
                        {
                            "Data": new_frame.data["Data"],
                            "Length": new_frame.data["Length"],
                        },
                    )
                    s = self.result_types[new_frame.type]["console"]
                    self.I3C_logger.info(s.format(**new_frame.data))

                return self.second_layer_analyzer.instruction_decoder(new_frame), DecodingModes.SDR

            except:
                self.I3C_logger.error("Invalid transaction")

            return self.second_layer_analyzer.instruction_decoder(new_frame), DecodingModes.SDR


        elif frame.type == "HOT-JOIN":
            # Clear temporary frame
            self.temp_frame = None

            # Return a new frame
            new_frame = AnalyzerFrame("Hot-Join", frame.start_time, frame.end_time, {})
            s = self.result_types[new_frame.type]["console"]
            try:
                # This will generate an error if there is any incomplete data in the frame (for example, an BC header without address)
                self.I3C_logger.info(s.format(**new_frame.data))
            except:
                new_frame = AnalyzerFrame(
                    "Incomplete Transaction",
                    new_frame.start_time,
                    new_frame.end_time,
                    {
                        "Data": new_frame.data["Data"],
                        "Length": new_frame.data["Length"],
                    },
                )
                s = self.result_types[new_frame.type]["console"]
                self.I3C_logger.info(s.format(**new_frame.data))

            return self.second_layer_analyzer.instruction_decoder(new_frame), DecodingModes.SDR

        elif frame.type == "ABORT":
            #ABORT is treated the same way as a STOP frame
            #----Message change to reflect the abort condition----#
            #Change message if the transaction supports abort frames
            if self.temp_frame.type == "Message" or self.temp_frame.type == "I3C Direct CCC Transaction" or self.temp_frame.type == "I3C Transaction" or self.temp_frame.type == "I3C IBI Transaction":
                #Change frame message to aborted message
                new_frame = self.second_layer_analyzer.handle_abort_message(self.temp_frame,frame.end_time)
            # Ignore if the transaction doesn't support abort frames
            else:
                self.I3C_logger.error("Abort unsupported for this frame type, Ignore")
                return None , DecodingModes.SDR

            #---Push the modified transaction frame---#
            # Clear temporary frame
            self.temp_frame = None

            #Check if theres frame is set up
            if (new_frame is not None):
                #Check if theres is accumulated data
                if new_frame.data["Length"] > 0:
                    new_frame.end_time = frame.end_time

                    #Check if all the fields are completed and push frame if possible
                    s = self.result_types[new_frame.type]['console']
                    try:
                        try:
                            # This will generate an error if there is any incomplete data in the frame (for example, an BC header without address)
                            self.I3C_logger.info(s.format(**new_frame.data))
                        except:
                            new_frame = AnalyzerFrame(
                                "Incomplete Transaction",
                                new_frame.start_time,
                                new_frame.end_time,
                                {
                                    "Data": new_frame.data["Data"],
                                    "Length": new_frame.data["Length"],
                                },
                            )
                            s = self.result_types[new_frame.type]["console"]
                            self.I3C_logger.info(s.format(**new_frame.data))
                        finally:
                            new_frame.data["Length"] = str(new_frame.data["Length"])

                        return self.second_layer_analyzer.instruction_decoder(new_frame), DecodingModes.SDR

                    except:
                        self.I3C_logger.error("Invalid transaction")
        elif frame.type == "UNSUPPORTED":
                self.temp_frame = None
                new_frame = AnalyzerFrame(
                    "UNSUPPORTED",
                    frame.start_time,
                    frame.end_time
                )
                return new_frame, DecodingModes.SDR
        
        elif self.temp_frame is not None and self.entdaa_devices is not None:
            # ENTDAA-specific frame accumulation: build per-device PID/BCR/DCR/DA info
            if frame.type == "DATA":
                data_byte = frame.data["Data"][0]
                self.entdaa_byte_count += 1
                if 1 <= self.entdaa_byte_count <= 6:
                    self.entdaa_current_device.setdefault("pid_bytes", []).append(data_byte)
                elif self.entdaa_byte_count == 7:
                    self.entdaa_current_device["bcr"] = data_byte
                elif self.entdaa_byte_count == 8:
                    self.entdaa_current_device["dcr"] = data_byte
            elif frame.type == "ADDRESS":
                self.entdaa_current_device["da"] = (frame.data.get("Address") or frame.data["Data"])[0]
                self.entdaa_devices.append(dict(self.entdaa_current_device))
                self.entdaa_current_device = {}
                self.entdaa_byte_count = 0
            # I3C BC ADDR re-broadcasts and any other frame type within ENTDAA: skip

        elif self.temp_frame is not None:
            # This LLA frame doesn't delimit a HLA transaction, and shall be accumulated if the temp frame exists
            # (that is, we're not getting a broken transaction on the first samples)

            # Add data byte, increment Length. Check the frame is not a TARGET RESET frame.
            if frame.type != "TARGET RESET":
                data_byte = frame.data["Data"][0]
                formatted_byte = "0x{0:0{1}X}".format(frame.data["Data"][0], 2)

                self.temp_frame.data["Length"] += 1
                if len(self.temp_frame.data["Data"]) > 0:
                    self.temp_frame.data["Data"] += ", "
                self.temp_frame.data["Data"] += formatted_byte

            # If this is an Address frame type, register the address
            if frame.type == "ADDRESS":
                if "Address" in self.temp_frame.data:
                    self.temp_frame.data["Address"] += ", "
                    self.temp_frame.data["W/R"] += ", "
                else:
                    self.temp_frame.data["Address"] = ""
                    self.temp_frame.data["W/R"] = ""
                self.temp_frame.data["Address"] += self._format_address(frame)
                self.temp_frame.data["W/R"] += frame.data["W/R"][0]

            if frame.type == "IBI ADDR":
                self.temp_frame.type = "I3C IBI Transaction"
                if "Address" in self.temp_frame.data:
                    self.temp_frame.data["Address"] += ", "
                    self.temp_frame.data["W/R"] += ", "
                else:
                    self.temp_frame.data["Address"] = ""
                    self.temp_frame.data["W/R"] = ""
                self.temp_frame.data["Address"] += self._format_address(frame)
                self.temp_frame.data["W/R"] += frame.data["W/R"][0]

            # The following logic is used to define I3C transaction types
            if (frame.type == "I3C BC ADDR") and (self.temp_frame.type == "Message"):
                self.temp_frame.type = "I3C Transaction"

            elif (frame.type == "CCC CMD") and (
                self.temp_frame.type == "I3C Transaction"
            ):
                # The sequence includes a CCC command definition. Check if it's a broadcast or direct command. Any BC has a CCC value less than 0x80

                if data_byte < 0x80:
                    self.temp_frame.data["Info"] = self.ccc_dict[data_byte]
                    self.temp_frame.type = "I3C BC CCC Transaction"
                else:
                    self.temp_frame.data["Info"] = self.ccc_dict[data_byte]
                    self.temp_frame.type = "I3C Direct CCC Transaction"

                # If the CCC is ENTDAA, switch to structured ENTDAA tracking
                if data_byte == 0x07:
                    self.temp_frame.type = "I3C ENTDAA Transaction"
                    self.entdaa_devices = []
                    self.entdaa_byte_count = 0
                    self.entdaa_current_device = {}

                # If the CCC is ENTHDR0 or ENTHDR3, commit current frame and switch mode
                for enthdr_byte, target_mode in [(0x20, DecodingModes.DDR), (0x23, DecodingModes.BT)]:
                    if data_byte == enthdr_byte:
                        new_frame = self.temp_frame
                        self.temp_frame = None

                        if new_frame is not None:
                            if new_frame.data["Length"] > 0:
                                new_frame.end_time = frame.end_time
                                s = self.result_types[new_frame.type]["console"]

                                try:
                                    try:
                                        self.I3C_logger.info(s.format(**new_frame.data))
                                    except:
                                        new_frame = AnalyzerFrame(
                                            "Incomplete Transaction",
                                            new_frame.start_time,
                                            new_frame.end_time,
                                            {
                                                "Data": new_frame.data["Data"],
                                                "Length": new_frame.data["Length"],
                                            },
                                        )
                                        s = self.result_types[new_frame.type]["console"]
                                        self.I3C_logger.info(s.format(**new_frame.data))
                                    finally:
                                        new_frame.data["Length"] = str(new_frame.data["Length"])

                                    return self.second_layer_analyzer.instruction_decoder(new_frame), target_mode

                                except:
                                    self.I3C_logger.error("Invalid transaction")

        # temp frame is None, and the Target Reset is encountered, which means it was emitted 
        # by the I3C controller when the bus was idle.        
        elif frame.type == "TARGET RESET": 
            # Return a new frame
            new_frame = AnalyzerFrame(
                "Target Reset Pattern",
                frame.start_time,
                frame.end_time,
                {"Info": frame.data["Info"]},
            )
            s = self.result_types[new_frame.type]["console"]
            try:
                try:
                    # This will generate an error if there is any incomplete data in the frame (for example, an BC header without address)
                    self.I3C_logger.info(s.format(**new_frame.data))
                except:
                    new_frame = AnalyzerFrame(
                        "Incomplete Transaction",
                        new_frame.start_time,
                        new_frame.end_time,
                        {
                            "Data": new_frame.data["Data"],
                            "Length": new_frame.data["Length"],
                        },
                    )
                    s = self.result_types[new_frame.type]["console"]
                    self.I3C_logger.info(s.format(**new_frame.data))

                return self.second_layer_analyzer.instruction_decoder(new_frame), DecodingModes.SDR

            except:
                self.I3C_logger.error("Invalid transaction")

            return self.second_layer_analyzer.instruction_decoder(new_frame), DecodingModes.SDR

        return None, DecodingModes.SDR

    @staticmethod
    def _decode_event_mask(byte):
        parts = []
        if byte & 0x08: parts.append("HJ")
        if byte & 0x02: parts.append("CR")
        if byte & 0x01: parts.append("IBI")
        return "/".join(parts) if parts else "none"

    @staticmethod
    def _decode_bcr_compact(bcr):
        role = (bcr >> 6) & 0x03
        parts = ["SecCtrl" if role == 1 else "Target"]
        if bcr & 0x20: parts.append("AdvCap")
        if bcr & 0x10: parts.append("VirtTgt")
        if bcr & 0x08: parts.append("Offline")
        if bcr & 0x02:
            parts.append("IBI+MDB" if bcr & 0x04 else "IBI")
        if bcr & 0x01: parts.append("SpeedLtd")
        return "/".join(parts)

    @staticmethod
    def _decode_bcr(bcr):
        role = (bcr >> 6) & 0x03
        role_text = {0: "I3C Target", 1: "I3C Controller Capable"}.get(role, f"Reserved ({role})")
        lines = [
            f"  [7:6] {role_text}",
            "  [5] " + ("Supports optional advanced capabilities" if bcr & 0x20 else "No advanced capabilities"),
            "  [4] " + ("Is a Virtual Target"                    if bcr & 0x10 else "Not a Virtual Target"),
            "  [3] " + ("Device may not always respond to I3C Bus commands" if bcr & 0x08 else "Device will always respond to I3C Bus commands"),
            "  [2] " + ("One or more data bytes follow the accepted IBI" if bcr & 0x04 else "No data bytes follow the accepted IBI"),
            "  [1] " + ("IBI request capable"     if bcr & 0x02 else "Not IBI request capable"),
            "  [0] " + ("Max data speed limitation" if bcr & 0x01 else "No max data speed limitation"),
        ]
        return "\n".join(lines)

    _ACTIVITY_MODE = {0: "Normal", 1: "Idle", 2: "Super Idle", 3: "DeepSleep"}

    @staticmethod
    def _decode_getstatus_verbose(status_msb, status_lsb):
        status_raw = (status_msb << 8) | status_lsb
        activity = (status_lsb >> 6) & 0x03
        activity_name = SDRDecoder._ACTIVITY_MODE.get(activity, f"Reserved ({activity})")
        perr = "Yes" if status_lsb & 0x20 else "No"
        ibi_count = status_lsb & 0x0F
        return "\n".join([
            f"  Status [0x{status_raw:04X}]:",
            f"    [15:8] (Reserved / Vendor-defined): 0x{status_msb:02X}",
            f"    [7:6]  Activity Mode:               {activity_name}",
            f"    [5]    Protocol Error:              {perr}",
            f"    [4]    (Reserved)",
            f"    [3:0]  Pending IBIs:                {ibi_count}",
        ])

    @staticmethod
    def _decode_maxwr_compact(b):
        rate = SDRDecoder._MAXWR_RATE.get(b & 0x07, f"0x{b & 0x07:X}")
        return f"WR:{rate}"

    @staticmethod
    def _decode_maxwr_verbose(b):
        freq_raw = (b >> 4) & 0x0F
        freq = f"{freq_raw}MHz" if freq_raw else "<1MHz"
        rate = SDRDecoder._MAXWR_RATE.get(b & 0x07, f"0x{b & 0x07:X}")
        db = "Yes" if b & 0x08 else "No"
        return "\n".join([
            f"  MaxWr: [0x{b:02X}]",
            f"    [7:4] MaxFreq:         {freq}",
            f"    [3]   DefByte Support: {db}",
            f"    [2:0] MaxWrRate:       {rate}",
        ])

    @staticmethod
    def _decode_maxrd_compact(b):
        rate = SDRDecoder._MAXRD_RATE.get(b & 0x07, f"0x{b & 0x07:X}")
        return f"RD:{rate}"

    @staticmethod
    def _decode_maxrd_verbose(b):
        w2r = "Yes" if b & 0x40 else "No"
        tsco = SDRDecoder._MAXRD_TSCO.get((b >> 3) & 0x07, "Reserved")
        rate = SDRDecoder._MAXRD_RATE.get(b & 0x07, f"0x{b & 0x07:X}")
        return "\n".join([
            f"  MaxRd: [0x{b:02X}]",
            f"    [7]   (Reserved)",
            f"    [6]   W2R Stop Permitted: {w2r}",
            f"    [5:3] tSCO:               {tsco}",
            f"    [2:0] MaxRdRate:          {rate}",
        ])

    @staticmethod
    def _decode_setxtime_compact(sc, data_bytes):
        short = SDRDecoder._SETXTIME_SUB_CMD.get(sc, f"0x{sc:02X} (Reserved)")
        if data_bytes:
            return f"Sub-Cmd: {short} " + " ".join(f"0x{b:02X}" for b in data_bytes)
        return short

    @staticmethod
    def _decode_setxtime_verbose(sc, data_bytes):
        _LONG = {
            0x7F: ("ST",      "Sync Tick — marks START as reference time for sync procedure"),
            0xBF: ("DT",      "Delay Time — elapsed time from correct T_Ph start to ST-marked START"),
            0xDF: ("Async0",  "Enter Async Mode 0 — Basic Asynchronous Timestamping"),
            0xEF: ("Async1",  "Enter Async Mode 1 — Advanced (uses aBE events to restrict counter range)"),
            0xF7: ("Async2",  "Enter Async Mode 2 — High-Precision Low-Power (SCL falling edges as SCLK)"),
            0xFB: ("Async3",  "Enter Async Mode 3 — High-Precision Triggerable (precision trigger + timestamp)"),
            0xFD: ("Trigger", "Async Trigger (Mode 3 only) — triggers active events after preset SCL+burst delay"),
            0x3F: ("TPH",     "Procedure Repetition Time — sets T_Ph synchronization event repetition period"),
            0x9F: ("TU",      "Time Unit — sets the time unit used for the DT command"),
            0x8F: ("ODR",     "Output Data Rate — sets Target's running output data rate"),
            0xFF: ("Exit",    "Exit — disables all enabled timing modes"),
        }
        short, desc = _LONG.get(sc, (f"0x{sc:02X}", "Reserved"))
        hex_part = f" (0x{sc:02X})" if sc in _LONG else ""
        lines = [f"  Sub-Cmd: {short}{hex_part} — {desc}"]
        if data_bytes:
            lines.append("  Data: " + " ".join(f"0x{b:02X}" for b in data_bytes))
        return "\n".join(lines)

    @staticmethod
    def _decode_setbuscon_compact(ctx, extra_bytes):
        if ctx == 0:
            label = "Reserved(0x00)"
        elif ctx <= 63:
            minor = ctx & 0x0F
            family = "Basic " if (ctx & 0x10) else ""
            rev = ".1+" if (ctx & 0x20) else ".0"
            label = f"I3C {family}v1.{minor}{rev}" if minor else f"I3C {family}v1.0(Illegal)"
        elif ctx <= 127:
            label = f"MIPIWGs(0x{ctx:02X})"
        elif ctx <= 191:
            label = f"StdOrg(0x{ctx:02X})"
        else:
            label = f"Vendor(0x{ctx:02X})"
        if extra_bytes:
            return label + " " + " ".join(f"0x{b:02X}" for b in extra_bytes)
        return label

    @staticmethod
    def _decode_setbuscon_verbose(ctx, extra_bytes):
        if ctx == 0:
            lines = ["  Context: 0x00 — Reserved, do not use"]
        elif ctx <= 63:
            minor    = ctx & 0x0F
            family   = "MIPI I3C Basic Specification" if (ctx & 0x10) else "MIPI I3C Specification"
            rev      = "v1.Y.1 or greater" if (ctx & 0x20) else "v1.Y.0"
            ver      = f"v1.{minor}" if minor else "Illegal (do not use)"
            lines = [
                f"  Context [0x{ctx:02X}]:",
                f"    [7:6] (Fixed 2'b00)",
                f"    [5]   Editorial Rev: {rev}",
                f"    [4]   Family:        {family}",
                f"    [3:0] Minor Version: {minor} → {ver}",
            ]
        elif ctx <= 127:
            lines = [f"  Context: 0x{ctx:02X} — Reserved for other MIPI Alliance Working Groups"]
        elif ctx <= 191:
            lines = [f"  Context: 0x{ctx:02X} — Reserved for other Standards Organizations (assigned by MIPI I3C WG)"]
        else:
            lines = [f"  Context: 0x{ctx:02X} — Vendor Custom (private, per-vendor use)"]
        if extra_bytes:
            lines.append("  Optional: " + " ".join(f"0x{b:02X}" for b in extra_bytes))
        return "\n".join(lines)

    # MLANE CCC (MIPI I3C v1.2 §6.7.3.3, Table 111) — the Defining Byte selects the I3C Mode
    # the lane configuration applies to (mirrors the ENTHDRx values; 0x00 = SDR).
    _MLANE_MODE = {
        0x00: "SDR",
        0x20: "HDR-DDR",
        0x21: "HDR-TSP",
        0x22: "HDR-TSL",
        0x23: "HDR-BT",
    }
    # Data Byte low 3 bits (Lane Config) -> (name, total lanes, additional lanes)
    _MLANE_LANE_CFG = {
        0x0: ("1-Lane (Default)", 1, 0),
        0x1: ("Dual", 2, 1),
        0x3: ("Quad", 4, 3),
    }
    # Recognized (mode, data byte) rows from Table 111 -> (ML Data Transfer name, section, table)
    _MLANE_FRAME_FORMATS = {
        ("SDR",     0x09): ("SDR-ML DUAL",     "6.7.3.4.2",   "Table 112"),
        ("SDR",     0x11): ("SDR-ML DUAL",     "6.7.3.4.2",   "Table 112"),
        ("SDR",     0x0B): ("SDR-ML QUAD",     "6.7.3.4.3",   "Table 112"),
        ("SDR",     0x13): ("SDR-ML QUAD",     "6.7.3.4.3",   "Table 112"),
        ("HDR-DDR", 0x09): ("HDR-DDR-ML DUAL", "6.7.3.5.1.2", "Table 121"),
        ("HDR-DDR", 0x11): ("HDR-DDR-ML DUAL", "6.7.3.5.1.2", "Table 121"),
        ("HDR-DDR", 0x0B): ("HDR-DDR-ML QUAD", "6.7.3.5.1.3", "Table 121"),
        ("HDR-DDR", 0x13): ("HDR-DDR-ML QUAD", "6.7.3.5.1.3", "Table 121"),
        ("HDR-TSP", 0x09): ("HDR-TSP-ML DUAL", "6.7.3.5.2.2", "Table 132"),
        ("HDR-TSP", 0x0B): ("HDR-TSP-ML QUAD", "6.7.3.5.2.3", "Table 132"),
        ("HDR-BT",  0x01): ("HDR-BT DUAL",     "6.7.3.5.3.2", "Table 143"),
        ("HDR-BT",  0x19): ("HDR-BT DUAL",     "6.7.3.5.3.2", "Table 143"),
        ("HDR-BT",  0x03): ("HDR-BT QUAD",     "6.7.3.5.3.3", "Table 143"),
        ("HDR-BT",  0x1B): ("HDR-BT QUAD",     "6.7.3.5.3.3", "Table 143"),
        ("HDR-BT",  0x3B): ("HDR-BT QUAD",     "6.7.3.5.3.3", "Table 143"),
    }

    @staticmethod
    def _decode_mlane_compact(defining_byte, data_byte):
        mode = SDRDecoder._MLANE_MODE.get(defining_byte, f"Mode 0x{defining_byte:02X}")
        if data_byte is None:
            return f"{mode} (Defining Byte 0x{defining_byte:02X})"
        coding = (data_byte >> 3) & 0x1F
        lane_cfg = data_byte & 0x07
        name, total, additional = SDRDecoder._MLANE_LANE_CFG.get(
            lane_cfg, (f"Reserved(0x{lane_cfg:X})", None, None))
        coding_note = ""
        if mode == "HDR-BT":
            coding_note = " (Compat)" if coding == 0 else " (Alt)"
        fmt = SDRDecoder._MLANE_FRAME_FORMATS.get((mode, data_byte))
        label = fmt[0] if fmt else f"{mode} {name}"
        if total:
            return f"{label} — Coding {coding}{coding_note}, +{additional} lanes ({total} total)"
        return f"{label} — Coding {coding}{coding_note}, LaneCfg 0x{lane_cfg:X}"

    @staticmethod
    def _decode_mlane_verbose(defining_byte, data_byte):
        mode = SDRDecoder._MLANE_MODE.get(defining_byte, f"Unknown (0x{defining_byte:02X})")
        lines = [f"  Mode: {mode} (Defining Byte 0x{defining_byte:02X})"]
        if data_byte is None:
            lines.append("  (No data byte — mode selector only)")
            return "\n".join(lines)
        coding = (data_byte >> 3) & 0x1F
        lane_cfg = data_byte & 0x07
        name, total, additional = SDRDecoder._MLANE_LANE_CFG.get(
            lane_cfg, (f"Reserved (0x{lane_cfg:X})", None, None))
        if mode == "HDR-BT":
            coding_desc = f"{coding} (Compatible Mode)" if coding == 0 else f"{coding} (Alternate Mode)"
        else:
            coding_desc = f"{coding}"
        if total:
            lane_desc = f"{name} (0x{lane_cfg:X}) → {total} lanes (+{additional} additional)"
        else:
            lane_desc = name
        lines.append(f"  Data Byte [0x{data_byte:02X}]:")
        lines.append(f"    [7:3] Coding Number: {coding_desc}")
        lines.append(f"    [2:0] Lane Config:   {lane_desc}")
        fmt = SDRDecoder._MLANE_FRAME_FORMATS.get((mode, data_byte))
        if fmt:
            ff_name, section, table = fmt
            lines.append(f"  Frame Format: {ff_name} — §{section}, {table}")
        else:
            lines.append("  Frame Format: Reserved/Unknown combination")
        return "\n".join(lines)

    @staticmethod
    def _decode_rstact_time(t):
        if t == 0xFF:
            return "indeterminate"
        amount = t & 0x7F
        if amount == 0:
            return "1 ms"
        return f"{amount} s" if t & 0x80 else f"{amount * 10} ms"

    @staticmethod
    def _decode_rstact_compact(db, data_bytes, is_bc=False):
        if db in SDRDecoder._RSTACT_ACTIONS:
            label = SDRDecoder._RSTACT_ACTIONS[db]
        elif db <= 0x3F:
            label = f"MIPIReserved(0x{db:02X})"
        elif db <= 0x7F:
            label = f"Vendor(0x{db:02X})"
        elif db == 0x81:
            label = "GetPeriphResetTime"
        elif db == 0x82:
            label = "GetWholeTargetResetTime"
        elif db == 0x83:
            label = "GetDebugAdapterResetTime"
        elif db == 0x84:
            label = "GetVirtualTargetInd"
        elif db <= 0xBF:
            label = f"GetMIPIReservedTime(0x{db:02X})"
        else:
            label = f"GetVendorTime(0x{db:02X})"
        if db <= 0x7F:
            if not is_bc and data_bytes:
                resp = data_bytes[0]
                curr_label = SDRDecoder._RSTACT_ACTIONS.get(resp, f"0x{resp:02X}")
                return f"GET {label} → Current:{curr_label}"
            return label if is_bc else f"SET {label}"
        if not data_bytes:
            return f"GET {label}"
        resp = data_bytes[0]
        if 0x81 <= db <= 0x83:
            return f"GET {label} → {SDRDecoder._decode_rstact_time(resp)}"
        if db == 0x84:
            return f"GET {label} → {'VirtualTarget' if resp else 'NotVirtual'}"
        return f"GET {label} → 0x{resp:02X}"

    @staticmethod
    def _decode_rstact_verbose(db, data_bytes, is_bc=False):
        _DB_DESC = {
            0x00: "No Reset on Target Reset Pattern",
            0x01: "Reset the I3C Peripheral Only",
            0x02: "Reset the Whole Target",
            0x03: "Debug Network Adapter Reset",
            0x04: "Virtual Target Detect (Direct CCC only)",
            0x81: "Return Time to Reset I3C Peripheral",
            0x82: "Return Time to Reset Whole Target",
            0x83: "Return Time for Debug Network Adapter Reset",
            0x84: "Return Virtual Target Indication",
        }
        if db in SDRDecoder._RSTACT_ACTIONS:
            label = SDRDecoder._RSTACT_ACTIONS[db]
        elif db == 0x81:
            label = "GetPeriphResetTime"
        elif db == 0x82:
            label = "GetWholeTargetResetTime"
        elif db == 0x83:
            label = "GetDebugAdapterResetTime"
        elif db == 0x84:
            label = "GetVirtualTargetInd"
        else:
            label = f"0x{db:02X}"
        desc = _DB_DESC.get(db)
        if desc is None:
            if 0x05 <= db <= 0x3F:
                desc = "Reserved by MIPI"
            elif 0x40 <= db <= 0x7F:
                desc = "Reserved for Vendors and external standards"
            elif 0x85 <= db <= 0xBF:
                desc = "Reserved for MIPI timing query (GET)"
            else:
                desc = "Reserved for Vendor/external standard timing query (GET)"
        lines = []
        if not is_bc:
            direction = "GET" if (db <= 0x7F and data_bytes) else ("SET" if db <= 0x7F else "GET")
            lines.append(f"  Direction: {direction}")
        lines.append(f"  Defining Byte: {label} (0x{db:02X}) — {desc}")
        if data_bytes:
            resp = data_bytes[0]
            if db <= 0x7F:
                curr_label = SDRDecoder._RSTACT_ACTIONS.get(resp, f"0x{resp:02X}")
                lines.append(f"  Response: 0x{resp:02X} → Currently set: {curr_label}")
            elif 0x81 <= db <= 0x83:
                lines.append(f"  Response: 0x{resp:02X} → {SDRDecoder._decode_rstact_time(resp)}")
            elif db == 0x84:
                lines.append(f"  Response: 0x{resp:02X} → {'Virtual Target' if resp else 'Not a Virtual Target'}")
            else:
                lines.append(f"  Response: 0x{resp:02X}")
        return "\n".join(lines)

    @staticmethod
    def _decode_getxtime_compact(sup, state, freq, inac):
        def _modes(byte):
            names = []
            if byte & 0x01: names.append("Sync")
            if byte & 0x02: names.append("Async0")
            if byte & 0x04: names.append("Async1")
            if byte & 0x08: names.append("Async2")
            if byte & 0x10: names.append("Async3")
            return "+".join(names) if names else "None"
        freq_str = "~32KHz" if freq == 0 else f"{freq * 0.5:.1f}MHz"
        inac_str = f"{inac * 0.1:.1f}%"
        return f"Sup:{_modes(sup)} | Act:{_modes(state)} Ovf:{'Yes' if state & 0x80 else 'No'} | Freq:{freq_str} | Inac:{inac_str}"

    @staticmethod
    def _decode_getxtime_verbose(sup, state, freq, inac):
        yn = lambda v: "Yes" if v else "No"
        en = lambda v: "Enabled" if v else "Disabled"

        def _mode_names(byte):
            names = []
            if byte & 0x01: names.append("Sync")
            if byte & 0x02: names.append("Async0")
            if byte & 0x04: names.append("Async1")
            if byte & 0x08: names.append("Async2")
            if byte & 0x10: names.append("Async3")
            return "+".join(names) if names else "None"

        freq_str = "~32 KHz" if freq == 0 else f"{freq * 0.5:.1f} MHz"
        inac_str = f"{inac * 0.1:.1f}%"
        ovf = bool(state & 0x80)
        supported_block = "\n".join([
            f"  Supported Modes [0x{sup:02X}]: {_mode_names(sup)}",
            f"    [0] Sync Mode:   {yn(sup & 0x01)}",
            f"    [1] Async Mode0: {yn(sup & 0x02)}",
            f"    [2] Async Mode1: {yn(sup & 0x04)}",
            f"    [3] Async Mode2: {yn(sup & 0x08)}",
            f"    [4] Async Mode3: {yn(sup & 0x10)}",
        ])
        state_block = "\n".join([
            f"  State Byte [0x{state:02X}]: Active={_mode_names(state & 0x1F)}  Overflow={yn(ovf)}",
            f"    [0] Sync Mode:   {en(state & 0x01)}",
            f"    [1] Async Mode0: {en(state & 0x02)}",
            f"    [2] Async Mode1: {en(state & 0x04)}",
            f"    [3] Async Mode2: {en(state & 0x08)}",
            f"    [4] Async Mode3: {en(state & 0x10)}",
            f"    [7] Overflow:    {yn(ovf)}",
        ])
        misc_block = "\n".join([
            f"  Frequency [0x{freq:02X}]: {freq_str}\n",
            f"  Inaccuracy [0x{inac:02X}]: {inac_str}",
        ])
        return "\n\n".join([supported_block, state_block, misc_block])

    @staticmethod
    def _decode_getcaps_compact(cap1, cap2=None, cap3=None, cap4=None):
        parts = []
        if cap2 is not None:
            ver = cap2 & 0x0F
            parts.append(f"v1.{ver}" if ver else "v1.?")
        modes = [m for b, m in [(0x01, "DDR"), (0x02, "TSP"), (0x04, "TSL"), (0x08, "BT")] if cap1 & b]
        parts.append("HDR:" + ("+".join(modes) if modes else "None"))
        if cap2 is not None:
            ga = SDRDecoder._GETCAPS_GA.get((cap2 >> 4) & 0x03, "?")
            parts.append(f"GA:{ga}")
            if cap2 & 0x80: parts.append("AbortCRC")
            if cap2 & 0x40: parts.append("WrAbort")
        if cap3 is not None:
            if cap3 & 0x80: parts.append("XTime")
            if cap3 & 0x40: parts.append("IBIRdNtf")
            if cap3 & 0x20: parts.append("BTcrc32")
            if cap3 & 0x10: parts.append("DBstat")
            if cap3 & 0x08: parts.append("DBcaps")
            if cap3 & 0x04: parts.append("D2DIBI")
            if cap3 & 0x02: parts.append("D2D")
            if cap3 & 0x01: parts.append("ML")
        if cap4 is not None:
            ccc = [m for b, m in [(0x01, "DDR"), (0x02, "TSP"), (0x04, "TSL"), (0x08, "BT")] if cap4 & b]
            parts.append("CCCs:" + ("+".join(ccc) if ccc else "None"))
        return " ".join(parts)

    @staticmethod
    def _decode_getcaps_verbose(cap1, cap2=None, cap3=None, cap4=None):
        yn = lambda v: "Yes" if v else "No"
        sections = []
        sections.append("\n".join([
            f"  GETCAP1 [0x{cap1:02X}]:",
            f"    [7:4] (Reserved)",
            f"    [3]   HDR-BT:  {yn(cap1 & 0x08)}",
            f"    [2]   HDR-TSL: {yn(cap1 & 0x04)}",
            f"    [1]   HDR-TSP: {yn(cap1 & 0x02)}",
            f"    [0]   HDR-DDR: {yn(cap1 & 0x01)}",
        ]))
        if cap2 is not None:
            ver = cap2 & 0x0F
            ga = SDRDecoder._GETCAPS_GA.get((cap2 >> 4) & 0x03, "?")
            sections.append("\n".join([
                f"  GETCAP2 [0x{cap2:02X}]:",
                f"    [7]   HDR-DDR Abort CRC:   {yn(cap2 & 0x80)}",
                f"    [6]   HDR-DDR Write Abort: {yn(cap2 & 0x40)}",
                f"    [5:4] Group Addresses:     {ga}",
                f"    [3:0] I3C Spec Version:    1.{ver}",
            ]))
        if cap3 is not None:
            sections.append("\n".join([
                f"  GETCAP3 [0x{cap3:02X}]:",
                f"    [7]   Timing Control:       {yn(cap3 & 0x80)}",
                f"    [6]   IBI Pending Read Ntf: {yn(cap3 & 0x40)}",
                f"    [5]   HDR-BT CRC-32:        {yn(cap3 & 0x20)}",
                f"    [4]   DefByte in GETSTATUS: {yn(cap3 & 0x10)}",
                f"    [3]   DefByte in GETCAPS:   {yn(cap3 & 0x08)}",
                f"    [2]   D2DXFER IBI Capable:  {yn(cap3 & 0x04)}",
                f"    [1]   D2DXFER Support:      {yn(cap3 & 0x02)}",
                f"    [0]   Multi-Lane:           {yn(cap3 & 0x01)}",
            ]))
        if cap4 is not None:
            sections.append("\n".join([
                f"  GETCAP4 [0x{cap4:02X}]:",
                f"    [7:4] (Reserved)",
                f"    [3]   HDR-BT CCCs:  {yn(cap4 & 0x08)}",
                f"    [2]   HDR-TSL CCCs: {yn(cap4 & 0x04)}",
                f"    [1]   HDR-TSP CCCs: {yn(cap4 & 0x02)}",
                f"    [0]   HDR-DDR CCCs: {yn(cap4 & 0x01)}",
            ]))
        return "\n".join(sections)

    @staticmethod
    def _parse_response_bytes(data_str):
        parts = [p.strip() for p in data_str.split(",") if p.strip()]
        return [int(b, 16) for b in parts[2:]]

    def _decode_direct_ccc_payload(self, frame):
        info = frame.data.get("Info", "")
        response = self._parse_response_bytes(frame.data.get("Data", ""))
        if info == "GETPID" and len(response) >= 7:
            pid_int = int.from_bytes(response[1:7], "big")
            frame.type = "I3C GETPID Transaction"
            frame.data["Data"] = f"0x{pid_int:012X}"
        elif info == "GETBCR" and len(response) >= 2:
            bcr = response[1]
            frame.type = "I3C GETBCR Transaction"
            frame.data["Data"] = f"{self._decode_bcr_compact(bcr)}"
            frame.data["DataVerbose"] = self._decode_bcr(bcr)
        elif info == "GETDCR" and len(response) >= 2:
            dcr = response[1]
            frame.type = "I3C GETDCR Transaction"
            frame.data["Data"] = f"{I3CDictionaries.dcr_dict.get(dcr, 'Reserved')}"
        elif info == "BC RSTDAA":
            frame.type = "I3C RSTDAA Transaction"
            frame.data["Data"] = ""
        elif info in ("BC ENTAS0", "BC ENTAS1", "BC ENTAS2", "BC ENTAS3"):
            frame.type = f"I3C BC {info[3:]} Transaction"
            frame.data["Data"] = ""
        elif info == "BC SETAASA":
            frame.type = "I3C BC SETAASA Transaction"
            frame.data["Data"] = ""
        elif info in ("ENTAS0", "ENTAS1", "ENTAS2", "ENTAS3"):
            frame.type = f"I3C {info} Transaction"
            frame.data["Data"] = ""
        elif info == "BC ENEC" and len(response) >= 1:
            ev = response[0]
            frame.type = "I3C BC ENEC Transaction"
            frame.data["Data"] = self._decode_event_mask(ev)
        elif info == "BC DISEC" and len(response) >= 1:
            ev = response[0]
            frame.type = "I3C BC DISEC Transaction"
            frame.data["Data"] = self._decode_event_mask(ev)
        elif info == "ENEC" and len(response) >= 2:
            ev = response[1]
            frame.type = "I3C ENEC Transaction"
            frame.data["Data"] = self._decode_event_mask(ev)
        elif info == "DISEC" and len(response) >= 2:
            ev = response[1]
            frame.type = "I3C DISEC Transaction"
            frame.data["Data"] = self._decode_event_mask(ev)
        elif info == "SETDASA" and len(response) >= 2:
            da = response[1]
            frame.type = "I3C SETDASA Transaction"
            for key in ("Address", "W/R"):
                val = frame.data.get(key, "")
                if ", " in val:
                    frame.data[key] = val.split(", ")[0]
            frame.data["Data"] = f"DA=0x{da:02X}"
        elif info == "SETNEWDA" and len(response) >= 2:
            da = response[1]
            frame.type = "I3C SETNEWDA Transaction"
            for key in ("Address", "W/R"):
                val = frame.data.get(key, "")
                if ", " in val:
                    frame.data[key] = val.split(", ")[0]
            frame.data["Data"] = f"DA=0x{da:02X}"
        elif info == "BC SETMWL" and len(response) >= 2:
            mwl = (response[0] << 8) | response[1]
            frame.type = "I3C BC SETMWL Transaction"
            frame.data["Data"] = f"{mwl} bytes"
        elif info in ("SETMWL", "GETMWL") and len(response) >= 3:
            mwl = (response[1] << 8) | response[2]
            frame.type = f"I3C {info} Transaction"
            frame.data["Data"] = f"{mwl} bytes"
        elif info == "BC SETMRL" and len(response) >= 2:
            mrl = (response[0] << 8) | response[1]
            ibi = str(response[2]) if len(response) >= 3 else "N/A"
            frame.type = "I3C BC SETMRL Transaction"
            frame.data["Data"] = f"{mrl} bytes (IBI={ibi})"
        elif info in ("SETMRL", "GETMRL") and len(response) >= 3:
            mrl = (response[1] << 8) | response[2]
            ibi = str(response[3]) if len(response) >= 4 else "N/A"
            frame.type = f"I3C {info} Transaction"
            frame.data["Data"] = f"{mrl} bytes (IBI={ibi})"
        elif info == "GETSTATUS" and len(response) >= 3:
            status_msb = response[1]
            status_lsb = response[2]
            status_raw = (status_msb << 8) | status_lsb
            activity = (status_lsb >> 6) & 0x03
            activity_name = self._ACTIVITY_MODE.get(activity, f"Rsvd({activity})")
            perr = "Yes" if status_lsb & 0x20 else "No"
            ibi = status_lsb & 0x0F
            frame.type = "I3C GETSTATUS Transaction"
            frame.data["Data"] = f"0x{status_raw:04X}(AM={activity_name} PERR={perr} IBI={ibi})"
            frame.data["DataVerbose"] = self._decode_getstatus_verbose(status_msb, status_lsb)
        elif info == "GETMXDS" and len(response) >= 3:
            maxwr, maxrd = response[1], response[2]
            compact = f"{self._decode_maxwr_compact(maxwr)} {self._decode_maxrd_compact(maxrd)}"
            verbose_parts = [self._decode_maxwr_verbose(maxwr), self._decode_maxrd_verbose(maxrd)]
            if len(response) >= 6:
                turn = response[3] | (response[4] << 8) | (response[5] << 16)
                compact += f" Turn:{turn}µs"
                verbose_parts.append(f"MaxRdTurn: {turn}µs")
            frame.type = "I3C GETMXDS Transaction"
            frame.data["Data"] = compact
            frame.data["DataVerbose"] = "\n\n".join(verbose_parts)
        elif info == "GETCAPS" and len(response) >= 2:
            cap1 = response[1]
            cap2 = response[2] if len(response) >= 3 else None
            cap3 = response[3] if len(response) >= 4 else None
            cap4 = response[4] if len(response) >= 5 else None
            frame.type = "I3C GETCAPS Transaction"
            frame.data["Data"] = self._decode_getcaps_compact(cap1, cap2, cap3, cap4)
            frame.data["DataVerbose"] = self._decode_getcaps_verbose(cap1, cap2, cap3, cap4)
        elif info == "BC SETXTIME" and len(response) >= 1:
            sc = response[0]
            frame.type = "I3C BC SETXTIME Transaction"
            frame.data["Data"] = self._decode_setxtime_compact(sc, response[1:])
            frame.data["DataVerbose"] = self._decode_setxtime_verbose(sc, response[1:])
        elif info == "SETXTIME" and len(response) >= 2:
            sc = response[1]
            frame.type = "I3C SETXTIME Transaction"
            frame.data["Data"] = self._decode_setxtime_compact(sc, response[2:])
            frame.data["DataVerbose"] = self._decode_setxtime_verbose(sc, response[2:])
        elif info == "GETXTIME" and len(response) >= 5:
            sup, state, freq, inac = response[1], response[2], response[3], response[4]
            frame.type = "I3C GETXTIME Transaction"
            frame.data["Data"] = self._decode_getxtime_compact(sup, state, freq, inac)
            frame.data["DataVerbose"] = self._decode_getxtime_verbose(sup, state, freq, inac)
        elif info == "GETXTIME" and len(response) >= 2:
            frame.type = "I3C GETXTIME Transaction"
            frame.data["Data"] = " ".join(f"0x{b:02X}" for b in response[1:])
        elif info == "BC RSTACT" and len(response) >= 1:
            frame.type = "I3C BC RSTACT Transaction"
            frame.data["Data"] = self._decode_rstact_compact(response[0], [], is_bc=True)
            frame.data["DataVerbose"] = self._decode_rstact_verbose(response[0], [], is_bc=True)
        elif info == "RSTACT" and len(response) >= 2:
            db = response[0]
            data_bytes = response[2:] if len(response) > 2 else []
            frame.type = "I3C RSTACT Transaction"
            frame.data["Data"] = self._decode_rstact_compact(db, data_bytes)
            frame.data["DataVerbose"] = self._decode_rstact_verbose(db, data_bytes)
        elif info == "BC RSTGRPA":
            frame.type = "I3C BC RSTGRPA Transaction"
            frame.data["Data"] = ""
        elif info == "RSTGRPA":
            frame.type = "I3C RSTGRPA Transaction"
            frame.data["Data"] = ""
        elif info == "SETGRPA" and len(response) >= 2:
            frame.type = "I3C SETGRPA Transaction"
            for key in ("Address", "W/R"):
                val = frame.data.get(key, "")
                if ", " in val:
                    frame.data[key] = val.split(", ")[0]
            frame.data["Data"] = f"GA=0x{response[1]:02X}"
        elif info == "BC SETBUSCON" and len(response) >= 1:
            ctx = response[0]
            frame.type = "I3C BC SETBUSCON Transaction"
            frame.data["Data"] = self._decode_setbuscon_compact(ctx, response[1:])
            frame.data["DataVerbose"] = self._decode_setbuscon_verbose(ctx, response[1:])
        elif info == "BC MLANE" and len(response) >= 1:
            # Broadcast MLANE: [defining byte, optional data byte]
            defbyte = response[0]
            databyte = response[1] if len(response) >= 2 else None
            frame.type = "I3C BC MLANE Transaction"
            frame.data["Data"] = self._decode_mlane_compact(defbyte, databyte)
            frame.data["DataVerbose"] = self._decode_mlane_verbose(defbyte, databyte)
        elif info == "MLANE" and len(response) >= 2:
            # Direct MLANE: [defining byte, target address, data byte]
            defbyte = response[0]
            databyte = response[2] if len(response) >= 3 else None
            frame.type = "I3C MLANE Transaction"
            frame.data["Data"] = self._decode_mlane_compact(defbyte, databyte)
            frame.data["DataVerbose"] = self._decode_mlane_verbose(defbyte, databyte)

    def _format_address(self, frame):
        # Mirror the LLA's "Address Display" setting: the LLA exports an already-
        # formatted "Address" column (7-bit with R/W removed, or the raw 8-bit byte).
        # Fall back to the raw Data byte for older LLA builds without the column.
        raw_byte = frame.data["Data"][0]
        addr_col = frame.data.get("Address")
        addr_val = addr_col[0] if addr_col else raw_byte
        formatted = "0x{0:02X}".format(addr_val)
        # In 8-bit mode the direction bit lives inside the byte and the LLA omits
        # the separate R/W indicator; detect that mode by the address equaling the
        # raw byte. In 7-bit mode (addr_val == raw_byte >> 1, so they differ) append
        # the direction.
        if addr_val != raw_byte:
            formatted += " - " + frame.data["W/R"][0]
        return formatted

    def _format_entdaa_devices(self, devices):
        parts = []
        for dev in devices:
            pid_bytes = dev.get("pid_bytes", [])
            pid_int = int.from_bytes(pid_bytes, "big") if pid_bytes else 0
            pid_str = f"0x{pid_int:012X}"
            bcr = dev.get("bcr", 0)
            dcr = dev.get("dcr", 0)
            da = dev.get("da", 0)
            dcr_desc = I3CDictionaries.dcr_dict.get(dcr, "Reserved")
            bcr_compact = self._decode_bcr_compact(bcr)
            parts.append(f"PID={pid_str} BCR=0x{bcr:02X}({bcr_compact}) DCR=0x{dcr:02X}({dcr_desc}) DA=0x{da:02X}")
        return " | ".join(parts)

    def _format_entdaa_devices_verbose(self, devices):
        parts = []
        for dev in devices:
            pid_bytes = dev.get("pid_bytes", [])
            pid_int = int.from_bytes(pid_bytes, "big") if pid_bytes else 0
            pid_str = f"0x{pid_int:012X}"
            bcr = dev.get("bcr", 0)
            dcr = dev.get("dcr", 0)
            da = dev.get("da", 0)
            dcr_desc = I3CDictionaries.dcr_dict.get(dcr, "Reserved")
            bcr_compact = self._decode_bcr_compact(bcr)
            bcr_verbose_lines = self._decode_bcr(bcr).split("\n")
            double_indented = "\n".join("  " + line for line in bcr_verbose_lines)
            parts.append(
                f"  PID={pid_str}\n"
                f"  BCR: [0x{bcr:02X}]\n"
                f"{double_indented}\n"
                f"  DCR=0x{dcr:02X}({dcr_desc})\n"
                f"  DA=0x{da:02X}"
            )
        return "\n\n".join(parts)
