Skip to content

Minimal Frame Parsing Guide

Overview

Minimal frames (PAYLOAD_MINIMAL) are the most lightweight framing option in struct-frame, using the format [MSG_ID] [PACKET] with no length field and no CRC checksum. They're ideal for:

  • Bandwidth-constrained links (LoRa, radio, RF)
  • Trusted communication (SPI, I2C, shared memory)
  • Fixed-size messages (sensor readings, control commands)
  • Low-power applications (battery-powered sensors)

Frame Structure

Minimal frames have three variants based on the header type:

BasicMinimal

[0x90] [0x70] [MSG_ID] [PAYLOAD]
  ▲      ▲       ▲         ▲
  │      │       │         └─ Your message data
  │      │       └─ Message ID (1 byte)
  │      └─ Payload type indicator (Minimal = 0x70)
  └─ Basic frame marker

Overhead: 3 bytes

TinyMinimal

[0x70] [MSG_ID] [PAYLOAD]
  ▲       ▲         ▲
  │       │         └─ Your message data
  │       └─ Message ID (1 byte)
  └─ Tiny+Minimal marker (0x70 = 0x70+0)

Overhead: 2 bytes

NoneMinimal

[MSG_ID] [PAYLOAD]
   ▲         ▲
   │         └─ Your message data
   └─ Message ID (1 byte)

Overhead: 1 byte

Key Requirement: Message Length Callback

Since minimal frames don't include a length field, you must provide a callback function that returns the expected message length for each message ID. Without this callback, the parser cannot determine where one message ends and the next begins.

Language-Specific Examples

Python

Note: The get_msg_length function is automatically generated by struct-frame based on your message definitions.

from parser import Parser, HeaderType, PayloadType
# Import the auto-generated get_msg_length function
from my_messages_sf import get_msg_length

# Create parser with auto-generated callback
parser = Parser(
    get_msg_length=get_msg_length,
    enabled_headers=[HeaderType.BASIC, HeaderType.TINY],
    enabled_payloads=[PayloadType.MINIMAL]
)

# Parse incoming bytes
for byte in incoming_data:
    result = parser.parse_byte(byte)
    if result.valid:
        print(f"Received msg_id={result.msg_id}, data={result.msg_data.hex()}")

# Encode a minimal frame
frame = parser.encode(
    msg_id=1,
    msg=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A',
    header_type=HeaderType.TINY,
    payload_type=PayloadType.MINIMAL
)
# Result: [0x70] [0x01] [0x01 0x02 ... 0x0A]

Manual callback (if needed): If you need custom logic, you can still define your own callback:

def get_msg_length(msg_id: int) -> int:
    """Return message length for each msg_id"""
    message_sizes = {
        1: 10,  # Status message is 10 bytes
        2: 20,  # Command message is 20 bytes
        3: 5,   # Sensor reading is 5 bytes
    }
    return message_sizes.get(msg_id, 0)

TypeScript

Note: The get_message_length function is automatically generated by struct-frame.

import { GenericFrameParser, FrameParserConfig, PayloadType } from './frame_base';
import { HEADER_TINY_CONFIG } from './frame_headers/header_tiny';
import { PAYLOAD_MINIMAL_CONFIG } from './payload_types/payload_minimal';
// Import the auto-generated function
import { get_message_length } from './my_messages.sf';

// Configure for TinyMinimal
const config: FrameParserConfig = {
    name: 'TinyMinimal',
    startBytes: [0x70],  // Tiny+Minimal
    headerSize: 2,       // start byte + msg_id
    footerSize: 0,       // no CRC
    hasLength: false,    // no length field
    lengthBytes: 0,
    hasCrc: false,
};

// Create parser with auto-generated callback
const parser = new GenericFrameParser(config, get_message_length);

// Parse incoming bytes
for (const byte of incomingData) {
    const result = parser.parse_byte(byte);
    if (result.valid) {
        console.log(`Received msg_id=${result.msg_id}, data=${result.msg_data}`);
    }
}

// Encode a minimal frame
const frame = parser.encode(1, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
// Result: [0x70] [0x01] [0x01 0x02 ... 0x0A]

C++

Note: The actual generated parser API may vary. This example shows the general pattern. Consult the generated code for your specific parser class constructor signature.

#include "frame_parsers.hpp"

using namespace FrameParsers;

// Define message size lookup
bool get_msg_length(uint8_t msg_id, size_t* length) {
    switch (msg_id) {
        case 1: *length = 10; return true;  // Status message
        case 2: *length = 20; return true;  // Command message
        case 3: *length = 5;  return true;  // Sensor reading
        default: return false;
    }
}

// Create buffer for parser
uint8_t buffer[256];

// Example pattern - actual API depends on generated code
// The parser may be created with a callback parameter
// or may have a separate method to set the callback
auto parser = TinyMinimalParser(buffer, sizeof(buffer), get_msg_length);

// Parse incoming bytes
for (uint8_t byte : incoming_data) {
    FrameMsgInfo result = parser.parse_byte(byte);
    if (result.valid) {
        printf("Received msg_id=%d, len=%zu\n", result.msg_id, result.msg_len);
        // Process result.msg_data
    }
}

// Encode a minimal frame
uint8_t msg_data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
size_t frame_len = parser.encode(1, msg_data, 10, buffer, sizeof(buffer));
// Result in buffer: [0x70] [0x01] [0x01 0x02 ... 0x0A]

C

#include "frame_parsers.h"

// Define message size lookup callback
bool get_msg_length(uint8_t msg_id, size_t* length) {
    switch (msg_id) {
        case 1: *length = 10; return true;  // Status message
        case 2: *length = 20; return true;  // Command message
        case 3: *length = 5;  return true;  // Sensor reading
        default: return false;
    }
}

// Create parser instance
uint8_t buffer[256];
// Note: Specific parser struct would be used
// This shows the pattern

// Parse incoming bytes
for (size_t i = 0; i < data_len; i++) {
    frame_msg_info_t result = parser_parse_byte(&parser, data[i]);
    if (result.valid) {
        printf("Received msg_id=%d, len=%zu\n", result.msg_id, result.msg_len);
        // Process result.msg_data
    }
}

// Encode a minimal frame
uint8_t msg_data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
uint8_t output[256];
size_t frame_len = encode_tiny_minimal(1, msg_data, 10, output, sizeof(output));
// Result in output: [0x70] [0x01] [0x01 0x02 ... 0x0A]

C

using StructFrame;

// Define message size lookup
int? GetMsgLength(int msgId)
{
    return msgId switch
    {
        1 => 10,  // Status message is 10 bytes
        2 => 20,  // Command message is 20 bytes
        3 => 5,   // Sensor reading is 5 bytes
        _ => null
    };
}

// Create parser with callback
var parser = new TinyMinimalParser(GetMsgLength);

// Parse incoming bytes
foreach (byte b in incomingData)
{
    var result = parser.ParseByte(b);
    if (result.Valid)
    {
        Console.WriteLine($"Received msg_id={result.MsgId}, data={BitConverter.ToString(result.MsgData)}");
    }
}

// Encode a minimal frame
byte[] msgData = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
byte[] frame = parser.Encode(1, msgData);
// Result: [0x70] [0x01] [0x01 0x02 ... 0x0A]

Common Patterns

Important: Struct-frame automatically generates the get_msg_length (Python) or get_message_length (TypeScript/C++/C#/C) function for you based on your message definitions. You typically don't need to write your own callback.

The auto-generated function looks up message sizes from your proto definitions. For example, if you have:

message StatusMessage {
  option msgid = 1;
  uint32 temperature = 1;
  uint16 battery = 2;
}

The generator will automatically include this message size in the lookup function.

Manual patterns (for advanced use cases only):

Pattern 1: Fixed-Size Messages

When all your messages are the same size:

def get_msg_length(msg_id: int) -> int:
    return 16  # All messages are 16 bytes

Pattern 2: Size Lookup Table

When you have multiple message types:

MESSAGE_SIZES = {
    1: 10,
    2: 20,
    3: 5,
    4: 100,
}

def get_msg_length(msg_id: int) -> int:
    return MESSAGE_SIZES.get(msg_id, 0)

Pattern 3: Generated from Proto

Automatically derive sizes from your message definitions:

from my_messages import StatusMessage, CommandMessage, SensorReading

MESSAGE_SIZES = {
    StatusMessage.msg_id: StatusMessage.msg_size,
    CommandMessage.msg_id: CommandMessage.msg_size,
    SensorReading.msg_id: SensorReading.msg_size,
}

def get_msg_length(msg_id: int) -> int:
    return MESSAGE_SIZES.get(msg_id, 0)

Choosing a Header Type

BasicMinimal (3-byte overhead)

  • Use when: You want robust framing with clear start markers
  • Best for: Serial ports, UART, general-purpose links
  • Example: [0x90] [0x70] [MSG_ID] [DATA]

TinyMinimal (2-byte overhead)

  • Use when: Every byte counts but you still want start markers
  • Best for: Low-power sensors, LoRa, RF links
  • Example: [0x70] [MSG_ID] [DATA]

NoneMinimal (1-byte overhead)

  • Use when: You have external synchronization or trust the link
  • Best for: SPI, I2C, shared memory, trusted IPC
  • Example: [MSG_ID] [DATA]

Pros and Cons

Advantages

Minimal overhead: 1-3 bytes total ✅ Fast encoding: No CRC calculation ✅ Fast decoding: No CRC validation ✅ Simple: Easy to understand and debug ✅ Efficient: Perfect for fixed-size messages

Disadvantages

No error detection: Corrupted data won't be detected ❌ Requires callback: Must provide message size lookup ❌ Less flexible: All messages of same ID must be same size ❌ Trusted links only: Use on reliable or error-corrected links

When to Use Minimal Frames

✅ Good Use Cases

  • Sensor networks: Temperature, humidity, pressure readings with fixed sizes
  • Control systems: Fixed-size command/response protocols
  • Embedded IPC: Board-to-board communication on PCB
  • LoRa/RF: Bandwidth is precious, messages are small and fixed
  • Real-time systems: Need lowest latency, have reliable links

❌ Avoid When

  • Unreliable links: Use PAYLOAD_DEFAULT with CRC instead
  • Variable-size messages: Use PAYLOAD_DEFAULT with length field
  • Unknown message sizes: Can't use minimal without size lookup
  • Error-prone environment: Need CRC validation

Upgrading to Default Payload

If you later need error detection or variable lengths:

# From Minimal
parser = Parser(
    get_msg_length=get_msg_length,
    enabled_payloads=[PayloadType.MINIMAL]
)

# To Default (adds length + CRC)
parser = Parser(
    enabled_payloads=[PayloadType.DEFAULT]
)
# No callback needed - length is in frame
# CRC provides error detection

Best Practices

  1. Document your message sizes: Keep a clear mapping of msg_id → size
  2. Validate on sender side: Check message is expected size before sending
  3. Use on trusted links: Don't use minimal on noisy serial links
  4. Consider upgrading: If reliability issues arise, upgrade to PAYLOAD_DEFAULT
  5. Test thoroughly: Without CRC, bugs can be harder to detect

Performance Comparison

Payload Type Overhead CRC Length Parsing Speed Best For
Minimal 1-3 bytes ⚡ Fastest Fixed-size, trusted links
Default 5-6 bytes 🚀 Fast General purpose
Extended 8+ bytes 🚀 Fast Large payloads, namespaces

See Also