Skip to content

Diagnostic report

This document describes the structure of the diagnostic report and how to decode it.

Packet Structure of the Diagnostic Report

When in diagnostic mode (or upon a report request in transparent mode), the modem sends an 18-byte report. The byte layout is:

Byte (bit) Field Description
0 (0:7) START_OF_FRAME (SOF) “$” (0x24)
1:2 (0:15) TB Transport block containing 2 bytes of received data.
3 (0:7) BER Bit error rate
4 (0:7) SIGNAL_POWER Relative signal power (0–255)
5 (0:7) NOISE_POWER Relative noise power (0–255)
6:7 (0:15) PACKET_VALID Indicates packet integrity: LDPC decode + CRC successful
8 (0:7) PACKET_INVALID CRC check failed
9 (0:7) GIT_REV Firmware revision (Git commit hash, 8-bit)
10:12 (0:23) TIME_FROM_BOOT Time since power-up in 10 ms increments
13:14 (0:15) CHIP_ID FPGA chip ID
15 (0:1) HW_REV Hardware revision (2 bits)
15 (2:5) CHANNEL Active communication channel (1–12)
15 (6) TB_VALID Transport block valid bit; indicates new TB data
15 (7) TX_COMPLETE Indicates that a transmission has finished
16 (0) DIAGNOSTIC_MODE 1 if in diagnostic mode; 0 otherwise
16 (1) RESERVED Reserved bit
16 (2:3) POWER_LEVEL Current power level (0 = L4, 1 = L3, 2 = L2, 3 = L1)
16 (4:7) RESERVED Reserved bits
17 (0:7) END_OF_FRAME (EOF) New line (“\n”)

Warning

Signal Power & Noise Power
- Both range from 0 to 255.
- When the modem is idle (not transmitting or receiving), Signal Power reflects the background noise floor, which should match Noise Power.
- During packet decoding, SIGNAL_POWER should be greater than NOISE_POWER for a successful decode.

Practical Tip
- Record the Noise Power while idle as your baseline (noise floor).
- Compare that with Signal Power during active communication.
- The bigger the gap, the better the link quality. (Could be closer to each other)


Example Python Report Parser

def decode_packet(packet: bytes) -> Optional[Dict[str, Any]]:
    """
    Decode a packet received from the modem.

    Parameters
    ----------
    packet : bytes
        The raw packet (18 bytes) starting with '$' (0x24) and ending with '\n' (0x0A).

    Returns
    -------
    Optional[Dict[str, Any]]
        A dictionary with decoded values if valid, or None otherwise.
    """
    if len(packet) != 18 or packet[0] != ord('$') or packet[-1] != ord('\n'):
        return None

    # The 16 bytes after the '$' and before the '\n'
    data = packet[1:17]
    decoded = struct.unpack("<HBBBHBBBBBHBB", data)

    decoded_dict = {
        "TR_BLOCK": decoded[0],
        "BER": decoded[1],
        "SIGNAL_POWER": decoded[2],
        "NOISE_POWER": decoded[3],
        "PACKET_VALID": decoded[4],
        "PACKET_INVALID": decoded[5],
        "GIT_REV": decoded[6].to_bytes(1, "little"),
        "TIME": (decoded[9] << 16) | (decoded[8] << 8) | decoded[7],
        "CHIP_ID": decoded[10],
        "HW_REV": decoded[11] & 0b00000011,
        "CHANNEL": (decoded[11] & 0b00111100) >> 2,
        "TB_VALID": (decoded[11] & 0b01000000) >> 6,
        "TX_COMPLETE": (decoded[11] & 0b10000000) >> 7,
        "DIAGNOSTIC_MODE": decoded[12] & 0b00000001,
        "LEVEL": (decoded[12] & 0b00001100) >> 2,
    }

    return decoded_dict