Skip to main content

MeshCore Stats Binary Frames

Stats Binary Frame Structures

Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.

Command Codes

CommandCodeDescription
CMD_GET_STATS56Get statistics (2-byte command: code + sub-type)

Stats Sub-Types

The CMD_GET_STATS command uses a 2-byte frame structure:

  • Byte 0: CMD_GET_STATS (56)
  • Byte 1: Stats sub-type:
  • STATS_TYPE_CORE (0) - Get core device statistics
  • STATS_TYPE_RADIO (1) - Get radio statistics
  • STATS_TYPE_PACKETS (2) - Get packet statistics

Response Codes

ResponseCodeDescription
RESP_CODE_STATS24Statistics response (2-byte response: code + sub-type)

Stats Response Sub-Types

The RESP_CODE_STATS response uses a 2-byte header structure:

  • Byte 0: RESP_CODE_STATS (24)
  • Byte 1: Stats sub-type (matches command sub-type):
  • STATS_TYPE_CORE (0) - Core device statistics response
  • STATS_TYPE_RADIO (1) - Radio statistics response
  • STATS_TYPE_PACKETS (2) - Packet statistics response

---

RESP_CODE_STATS + STATS_TYPE_CORE (24, 0)

Total Frame Size: 11 bytes

OffsetSizeTypeField NameDescriptionRange/Notes
01uint8_tresponse_codeAlways 0x18 (24)-
11uint8_tstats_typeAlways 0x00 (STATS_TYPE_CORE)-
22uint16_tbattery_mvBattery voltage in millivolts0 - 65,535
44uint32_tuptime_secsDevice uptime in seconds0 - 4,294,967,295
82uint16_terrorsError flags bitmask-
101uint8_tqueue_lenOutbound packet queue length0 - 255

Example Structure (C/C++)

struct StatsCore {
    uint8_t  response_code;  // 0x18
    uint8_t  stats_type;     // 0x00 (STATS_TYPE_CORE)
    uint16_t battery_mv;
    uint32_t uptime_secs;
    uint16_t errors;
    uint8_t  queue_len;
} __attribute__((packed));

---

RESP_CODE_STATS + STATS_TYPE_RADIO (24, 1)

Total Frame Size: 14 bytes

OffsetSizeTypeField NameDescriptionRange/Notes
01uint8_tresponse_codeAlways 0x18 (24)-
11uint8_tstats_typeAlways 0x01 (STATS_TYPE_RADIO)-
22int16_tnoise_floorRadio noise floor in dBm-140 to +10
41int8_tlast_rssiLast received signal strength in dBm-128 to +127
51int8_tlast_snrSNR scaled by 4Divide by 4.0 for dB
64uint32_ttx_air_secsCumulative transmit airtime in seconds0 - 4,294,967,295
104uint32_trx_air_secsCumulative receive airtime in seconds0 - 4,294,967,295

Example Structure (C/C++)

struct StatsRadio {
    uint8_t  response_code;  // 0x18
    uint8_t  stats_type;     // 0x01 (STATS_TYPE_RADIO)
    int16_t  noise_floor;
    int8_t   last_rssi;
    int8_t   last_snr;       // Divide by 4.0 to get actual SNR in dB
    uint32_t tx_air_secs;
    uint32_t rx_air_secs;
} __attribute__((packed));

---

RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)

Total Frame Size: 26 bytes (legacy) or 30 bytes (includes recv_errors)

OffsetSizeTypeField NameDescriptionRange/Notes
01uint8_tresponse_codeAlways 0x18 (24)-
11uint8_tstats_typeAlways 0x02 (STATS_TYPE_PACKETS)-
24uint32_trecvTotal packets received0 - 4,294,967,295
64uint32_tsentTotal packets sent0 - 4,294,967,295
104uint32_tflood_txPackets sent via flood routing0 - 4,294,967,295
144uint32_tdirect_txPackets sent via direct routing0 - 4,294,967,295
184uint32_tflood_rxPackets received via flood routing0 - 4,294,967,295
224uint32_tdirect_rxPackets received via direct routing0 - 4,294,967,295
264uint32_trecv_errorsReceive/CRC errors (RadioLib); present only in 30-byte frame0 - 4,294,967,295

Notes

  • Counters are cumulative from boot and may wrap.
  • recv = flood_rx + direct_rx
  • sent = flood_tx + direct_tx
  • Clients should accept frame length ≥ 26; if length ≥ 30, parse recv_errors at offset 26.

Example Structure (C/C++)

struct StatsPackets {
    uint8_t  response_code;  // 0x18
    uint8_t  stats_type;     // 0x02 (STATS_TYPE_PACKETS)
    uint32_t recv;
    uint32_t sent;
    uint32_t flood_tx;
    uint32_t direct_tx;
    uint32_t flood_rx;
    uint32_t direct_rx;
    uint32_t recv_errors;    // present when frame size is 30
} __attribute__((packed));

---

Command Usage Example (Python)

# Send CMD_GET_STATS command
def send_get_stats_core(serial_interface):
    """Send command to get core stats"""
    cmd = bytes([56, 0])  # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)
    serial_interface.write(cmd)

def send_get_stats_radio(serial_interface):
    """Send command to get radio stats"""
    cmd = bytes([56, 1])  # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)
    serial_interface.write(cmd)

def send_get_stats_packets(serial_interface):
    """Send command to get packet stats"""
    cmd = bytes([56, 2])  # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)
    serial_interface.write(cmd)

---

Response Parsing Example (Python)

import struct

def parse_stats_core(frame):
    """Parse RESP_CODE_STATS + STATS_TYPE_CORE frame (11 bytes)"""
    response_code, stats_type, battery_mv, uptime_secs, errors, queue_len = \
        struct.unpack('<B B H I H B', frame)
    assert response_code == 24 and stats_type == 0, "Invalid response type"
    return {
        'battery_mv': battery_mv,
        'uptime_secs': uptime_secs,
        'errors': errors,
        'queue_len': queue_len
    }

def parse_stats_radio(frame):
    """Parse RESP_CODE_STATS + STATS_TYPE_RADIO frame (14 bytes)"""
    response_code, stats_type, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs = \
        struct.unpack('<B B h b b I I', frame)
    assert response_code == 24 and stats_type == 1, "Invalid response type"
    return {
        'noise_floor': noise_floor,
        'last_rssi': last_rssi,
        'last_snr': last_snr / 4.0,  # Unscale SNR
        'tx_air_secs': tx_air_secs,
        'rx_air_secs': rx_air_secs
    }

def parse_stats_packets(frame):
    """Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 bytes)"""
    assert len(frame) >= 26, "STATS_TYPE_PACKETS frame too short"
    response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \
        struct.unpack('<B B I I I I I I', frame[:26])
    assert response_code == 24 and stats_type == 2, "Invalid response type"
    result = {
        'recv': recv,
        'sent': sent,
        'flood_tx': flood_tx,
        'direct_tx': direct_tx,
        'flood_rx': flood_rx,
        'direct_rx': direct_rx
    }
    if len(frame) >= 30:
        (recv_errors,) = struct.unpack('<I', frame[26:30])
        result['recv_errors'] = recv_errors
    return result

---

Command Usage Example (JavaScript/TypeScript)

// Send CMD_GET_STATS command
const CMD_GET_STATS = 56;
const STATS_TYPE_CORE = 0;
const STATS_TYPE_RADIO = 1;
const STATS_TYPE_PACKETS = 2;

function sendGetStatsCore(serialInterface: SerialPort): void {
    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_CORE]);
    serialInterface.write(cmd);
}

function sendGetStatsRadio(serialInterface: SerialPort): void {
    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_RADIO]);
    serialInterface.write(cmd);
}

function sendGetStatsPackets(serialInterface: SerialPort): void {
    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_PACKETS]);
    serialInterface.write(cmd);
}

---

Response Parsing Example (JavaScript/TypeScript)

interface StatsCore {
    battery_mv: number;
    uptime_secs: number;
    errors: number;
    queue_len: number;
}

interface StatsRadio {
    noise_floor: number;
    last_rssi: number;
    last_snr: number;
    tx_air_secs: number;
    rx_air_secs: number;
}

interface StatsPackets {
    recv: number;
    sent: number;
    flood_tx: number;
    direct_tx: number;
    flood_rx: number;
    direct_rx: number;
    recv_errors?: number;  // present when frame is 30 bytes
}

function parseStatsCore(buffer: ArrayBuffer): StatsCore {
    const view = new DataView(buffer);
    const response_code = view.getUint8(0);
    const stats_type = view.getUint8(1);
    if (response_code !== 24 || stats_type !== 0) {
        throw new Error('Invalid response type');
    }
    return {
        battery_mv: view.getUint16(2, true),
        uptime_secs: view.getUint32(4, true),
        errors: view.getUint16(8, true),
        queue_len: view.getUint8(10)
    };
}

function parseStatsRadio(buffer: ArrayBuffer): StatsRadio {
    const view = new DataView(buffer);
    const response_code = view.getUint8(0);
    const stats_type = view.getUint8(1);
    if (response_code !== 24 || stats_type !== 1) {
        throw new Error('Invalid response type');
    }
    return {
        noise_floor: view.getInt16(2, true),
        last_rssi: view.getInt8(4),
        last_snr: view.getInt8(5) / 4.0,  // Unscale SNR
        tx_air_secs: view.getUint32(6, true),
        rx_air_secs: view.getUint32(10, true)
    };
}

function parseStatsPackets(buffer: ArrayBuffer): StatsPackets {
    const view = new DataView(buffer);
    if (buffer.byteLength < 26) {
        throw new Error('STATS_TYPE_PACKETS frame too short');
    }
    const response_code = view.getUint8(0);
    const stats_type = view.getUint8(1);
    if (response_code !== 24 || stats_type !== 2) {
        throw new Error('Invalid response type');
    }
    const result: StatsPackets = {
        recv: view.getUint32(2, true),
        sent: view.getUint32(6, true),
        flood_tx: view.getUint32(10, true),
        direct_tx: view.getUint32(14, true),
        flood_rx: view.getUint32(18, true),
        direct_rx: view.getUint32(22, true)
    };
    if (buffer.byteLength >= 30) {
        result.recv_errors = view.getUint32(26, true);
    }
    return result;
}

---

Field Size Considerations

  • Packet counters (uint32_t): May wrap after extended high-traffic operation.
  • Time fields (uint32_t): Max ~136 years.
  • SNR (int8_t, scaled by 4): Range -32 to +31.75 dB, 0.25 dB precision.