diff --git a/docs/user-guide/framing-architecture.md b/docs/user-guide/framing-architecture.md index 25351803..6e247f76 100644 --- a/docs/user-guide/framing-architecture.md +++ b/docs/user-guide/framing-architecture.md @@ -1,6 +1,44 @@ # Framing Architecture -This document provides detailed technical information about the struct-frame framing system architecture, payload types, frame types, and complete format reference. +This document provides technical information about the struct-frame framing system architecture, including the composable design using language-agnostic frame format definitions. + +## Overview + +struct-frame uses a **composable frame format architecture** where complete frame formats are built by combining: + +1. **Headers**: Define start byte patterns for synchronization +2. **Payloads**: Define the structure of metadata fields (length, CRC, etc.) +3. **Profiles**: Predefined combinations for common use cases + +This architecture allows for flexible customization while providing simple, intent-based profiles for most users. + +## Language-Agnostic Definitions + +Frame formats are defined in Python modules under `src/struct_frame/frame_formats/`: + +``` +frame_formats/ +├── __init__.py # Public API +├── base.py # Core data structures (HeaderType, PayloadType, etc.) +├── headers.py # Header definitions (None, Tiny, Basic, etc.) +├── payloads.py # Payload definitions (Minimal, Default, Extended, etc.) +├── profiles.py # Standard profile definitions +└── crc.py # CRC calculation utilities +``` + +**Key Benefits**: +- ✓ Composable: Complex payloads build upon simpler ones +- ✓ Language-agnostic: Definitions describe byte-level structures +- ✓ Maintainable: Each concept in its own file +- ✓ Extensible: Easy to add new types via PR + +## Simplified Proto File + +The `frame_formats.proto` file contains **only**: +- Enumerations: `HeaderType`, `PayloadType`, `Profile` +- Profile aliases for custom combinations + +The proto file does not contain message definitions for each frame format variant. ## Framing Architecture @@ -17,11 +55,11 @@ The framing system uses a two-level architecture: ## Frame Format Definitions -All supported frame formats are defined in [`examples/frame_formats.proto`](https://github.com/mylonics/struct-frame/blob/main/examples/frame_formats.proto). This file provides: +Frame format definitions are in language-agnostic Python modules. This allows for code reuse, better documentation, and type safety. -- Protocol Buffer definitions for each frame format -- Enumeration of frame types and payload types -- Configuration message for runtime format selection +For examples and usage, see: +- [`src/struct_frame/frame_formats/`](https://github.com/mylonics/struct-frame/tree/main/src/struct_frame/frame_formats) - Definition source code +- [Frame Profile Calculator](framing-calculator.md) - Interactive tool to explore formats ## Start Byte Scheme diff --git a/docs/user-guide/framing-calculator.md b/docs/user-guide/framing-calculator.md index fa621823..d69f1f39 100644 --- a/docs/user-guide/framing-calculator.md +++ b/docs/user-guide/framing-calculator.md @@ -390,9 +390,68 @@ Footer: ### Next Steps -1. **Copy the frame format name** (e.g., `BasicDefault`) -2. **Use it in your code** when initializing the frame parser -3. **Reference the profile** in documentation for clarity +1. **Use a standard profile** if it matches your needs (recommended) +2. **Or create a custom profile alias** in a `frame_format.proto` file +3. **Use it in your code** when initializing the frame parser + +### Using Standard Profiles + +For most use cases, use one of the 5 standard profiles: + +```protobuf +// Use this in your frame_format.proto file or directly in code + +// For general serial/UART: +Profile.STANDARD // Maps to BasicDefault + +// For low-bandwidth sensors: +Profile.SENSOR // Maps to TinyMinimal + +// For trusted inter-process communication: +Profile.IPC // Maps to NoneMinimal + +// For large file transfers: +Profile.BULK // Maps to BasicExtended + +// For multi-node mesh networks: +Profile.NETWORK // Maps to BasicExtendedMultiSystemStream +``` + +### Creating Custom Profiles + +If you need a custom combination of features, create a `frame_format.proto` file with a profile alias: + +```protobuf +// frame_format.proto +package frame_formats; + +message ProfileAlias { + string name = 1; + HeaderType header_type = 2; + PayloadType payload_type = 3; + string description = 4; +} + +// Example: Tiny header with Default payload (lower overhead than Standard) +message TinyDefaultAlias { + ProfileAlias profile = 1 [default = { + name: "TinyDefault", + header_type: HEADER_TINY, + payload_type: PAYLOAD_DEFAULT, + description: "Tiny header with default payload" + }]; +} +``` + +Then generate code with: +```bash +struct-frame your_messages.proto --frame_formats frame_format.proto --build_py +``` + +### Contributing New Headers/Payloads + +If you need a new header or payload type that doesn't exist, please submit a PR to the +[struct-frame repository](https://github.com/mylonics/struct-frame). #### Example Usage diff --git a/docs/user-guide/framing.md b/docs/user-guide/framing.md index 27a07194..5149152d 100644 --- a/docs/user-guide/framing.md +++ b/docs/user-guide/framing.md @@ -19,6 +19,21 @@ Instead of choosing individual frame features, **use these intent-based profiles !!! tip "Interactive Calculator" Use the [Frame Profile Calculator](framing-calculator.md) to select features and see which profile matches your needs! +!!! example "Using Profiles in Code" + ```python + from struct_frame.frame_formats import Profile, get_profile + + # Get a standard profile + standard = get_profile(Profile.STANDARD) + print(f"Overhead: {standard.total_overhead} bytes") # 6 bytes + + # Or create a custom profile + from struct_frame.frame_formats import create_custom_profile, HeaderType, PayloadType + custom = create_custom_profile("TinyDefault", HeaderType.TINY, PayloadType.DEFAULT) + ``` + + See [examples/frame_format_profiles.py](https://github.com/mylonics/struct-frame/blob/main/examples/frame_format_profiles.py) for more examples. + ### Choose Your Frame: Decision Tree ``` diff --git a/examples/frame_format_profiles.py b/examples/frame_format_profiles.py new file mode 100644 index 00000000..6c1c6e54 --- /dev/null +++ b/examples/frame_format_profiles.py @@ -0,0 +1,205 @@ +""" +Example: Using Frame Format Profiles + +This example demonstrates how to use the 5 standard frame format profiles +and how to create custom profile combinations. +""" + +import sys +import os + +# Add src to path for the example +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from struct_frame.frame_formats import ( + Profile, + get_profile, + list_profiles, + create_custom_profile, + HeaderType, + PayloadType, +) + + +def example_standard_profiles(): + """Show how to use standard profiles""" + print("=" * 70) + print("STANDARD PROFILES") + print("=" * 70) + + print("\nUse these 5 profiles for most applications:\n") + + for profile in Profile: + definition = get_profile(profile) + print(f"{profile.name:15} → {definition.name:35} ({definition.total_overhead:2} bytes)") + print(f" Use case: {definition.description[:60]}...") + print() + + +def example_profile_details(): + """Show detailed information about a profile""" + print("=" * 70) + print("PROFILE DETAILS: NETWORK") + print("=" * 70) + + network = get_profile(Profile.NETWORK) + + print(f"\nFrame Format: {network.name}") + print(f"Description: {network.description}") + print() + + print(f"Header: {network.header.name}") + print(f" Start bytes: {network.header.num_start_bytes}") + print(f" Encodes payload type: {network.header.encodes_payload_type}") + if network.start_byte_value: + print(f" Start byte value: 0x{network.start_byte_value:02X}") + print() + + print(f"Payload: {network.payload.name}") + print(f" Has CRC: {network.payload.has_crc} ({network.payload.crc_bytes} bytes)") + print(f" Has Length: {network.payload.has_length} ({network.payload.length_bytes} bytes)") + print(f" Has Sequence: {network.payload.has_sequence}") + print(f" Has System ID: {network.payload.has_system_id}") + print(f" Has Component ID: {network.payload.has_component_id}") + print(f" Has Package ID: {network.payload.has_package_id}") + print(f" Field order: {network.payload.get_field_order()}") + print() + + print(f"Total Overhead: {network.total_overhead} bytes") + max_payload = 65535 if network.payload.length_bytes == 2 else 255 if network.payload.has_length else "No limit (fixed size)" + print(f"Max Payload Size: {max_payload} bytes") + + +def example_custom_profiles(): + """Show how to create custom profile combinations""" + print("\n" + "=" * 70) + print("CUSTOM PROFILES") + print("=" * 70) + + print("\nCreate custom combinations when the standard profiles don't fit:\n") + + # Example 1: TinyDefault - Lower overhead than Standard + tiny_default = create_custom_profile( + "TinyDefault", + HeaderType.TINY, + PayloadType.DEFAULT, + "Lower overhead alternative to Standard profile" + ) + print(f"1. {tiny_default.name}") + print(f" Header: {tiny_default.header.name} (1 byte)") + print(f" Payload: {tiny_default.payload.name} (4 bytes)") + print(f" Total overhead: {tiny_default.total_overhead} bytes") + print(f" Start byte: 0x{tiny_default.start_byte_value:02X}") + print(f" Use case: {tiny_default.description}") + print() + + # Example 2: BasicSeq - Standard with packet loss detection + basic_seq = create_custom_profile( + "BasicSeq", + HeaderType.BASIC, + PayloadType.SEQ, + "Standard profile with packet loss detection" + ) + print(f"2. {basic_seq.name}") + print(f" Header: {basic_seq.header.name} (2 bytes)") + print(f" Payload: {basic_seq.payload.name} (5 bytes)") + print(f" Total overhead: {basic_seq.total_overhead} bytes") + print(f" Has sequence: {basic_seq.payload.has_sequence}") + print(f" Use case: {basic_seq.description}") + print() + + # Example 3: NoneDefault - IPC with error detection + none_default = create_custom_profile( + "NoneDefault", + HeaderType.NONE, + PayloadType.DEFAULT, + "Trusted IPC with CRC error detection" + ) + print(f"3. {none_default.name}") + print(f" Header: {none_default.header.name} (0 bytes)") + print(f" Payload: {none_default.payload.name} (4 bytes)") + print(f" Total overhead: {none_default.total_overhead} bytes") + print(f" Use case: {none_default.description}") + + +def example_profile_comparison(): + """Compare overhead across different profiles""" + print("\n" + "=" * 70) + print("PROFILE COMPARISON") + print("=" * 70) + + print("\nOverhead comparison for different use cases:\n") + print(f"{'Profile':<20} {'Frame Format':<35} {'Overhead':>10} {'Max Payload':>15}") + print("-" * 80) + + for profile in Profile: + definition = get_profile(profile) + max_payload = ( + f"{65535} bytes" if definition.payload.length_bytes == 2 + else f"{255} bytes" if definition.payload.has_length + else "Fixed size" + ) + print(f"{profile.name:<20} {definition.name:<35} {definition.total_overhead:>10} {max_payload:>15}") + + # Add some custom examples + print() + print("Custom examples:") + tiny_default = create_custom_profile("TinyDefault", HeaderType.TINY, PayloadType.DEFAULT, "") + print(f"{'TinyDefault':<20} {tiny_default.name:<35} {tiny_default.total_overhead:>10} {'255 bytes':>15}") + + basic_seq = create_custom_profile("BasicSeq", HeaderType.BASIC, PayloadType.SEQ, "") + print(f"{'BasicSeq':<20} {basic_seq.name:<35} {basic_seq.total_overhead:>10} {'255 bytes':>15}") + + +def example_frame_structure(): + """Show the byte-level structure of different profiles""" + print("\n" + "=" * 70) + print("FRAME STRUCTURE") + print("=" * 70) + + # STANDARD profile + standard = get_profile(Profile.STANDARD) + print(f"\n{Profile.STANDARD.name} ({standard.name}):") + print(" [0x90] [0x71] [LEN] [MSG_ID] [PAYLOAD] [CRC1] [CRC2]") + print(" └─┬──┘ └─┬──┘ └┬──┘ └──┬──┘ │ └────┬────┘") + print(" │ │ │ │ │ │") + print(" Start Start 1-byte Message Message CRC-16") + print(" byte1 byte2 length ID data checksum") + + # SENSOR profile + sensor = get_profile(Profile.SENSOR) + print(f"\n{Profile.SENSOR.name} ({sensor.name}):") + print(" [0x70] [MSG_ID] [PAYLOAD]") + print(" └─┬─┘ └──┬──┘ │") + print(" │ │ │") + print(" Start Message Message") + print(" byte ID data (fixed size)") + + # NETWORK profile + network = get_profile(Profile.NETWORK) + print(f"\n{Profile.NETWORK.name} ({network.name}):") + print(" [0x90] [0x78] [SEQ] [SYS] [COMP] [LEN_LO] [LEN_HI] [PKG] [MSG_ID] [PAYLOAD] [CRC1] [CRC2]") + print(" └─┬──┘ └─┬──┘ │ │ │ └──────┬──────┘ │ │ │ └────┬────┘") + print(" │ │ │ │ │ │ │ │ │ │") + print(" Start Start Seq System Component 16-bit Package Message Message CRC-16") + print(" byte1 byte2 num ID ID length ID ID data checksum") + + +def main(): + """Run all examples""" + example_standard_profiles() + example_profile_details() + example_custom_profiles() + example_profile_comparison() + example_frame_structure() + + print("\n" + "=" * 70) + print("For more information, see:") + print(" - docs/user-guide/framing.md") + print(" - docs/user-guide/framing-calculator.md") + print(" - docs/user-guide/framing-architecture.md") + print("=" * 70) + + +if __name__ == '__main__': + main() diff --git a/examples/frame_formats.proto b/examples/frame_formats.proto index 0e1c848e..c6ede858 100644 --- a/examples/frame_formats.proto +++ b/examples/frame_formats.proto @@ -1,26 +1,58 @@ -// Frame Format Definitions for struct-frame -// Defines message framing formats for communication over serial, network, or byte streams. +// Frame Format Profiles for struct-frame // -// FRAMING ARCHITECTURE: -// The framing system has two levels: -// 1. FRAMER (Basic/Tiny/None): Determines start bytes for synchronization -// - Basic: 2 start bytes [0x90] [0x70+payload_type] -// - Tiny: 1 start byte [0x70+payload_type] -// - None: 0 start bytes (relies on external sync) +// This file defines the standard frame format profiles and allows users to create +// custom profile aliases by combining HeaderType and PayloadType. // -// 2. PAYLOAD TYPE: Defines the header/footer structure of the payload -// The second start byte of Basic (or the single start byte of Tiny) encodes the payload type. +// STANDARD PROFILES: +// Instead of manually configuring headers and payloads, use these intent-based +// profiles that bundle common features for your use case: // -// Start byte 1 (Basic only): 0x90 -// Start byte 2 (Basic) or Start byte (Tiny): 0x70 + PayloadType offset +// - STANDARD: General purpose serial/UART (BasicDefault) +// - SENSOR: Low-bandwidth sensors (TinyMinimal) +// - IPC: Trusted inter-process communication (NoneMinimal) +// - BULK: Large file transfers (BasicExtended) +// - NETWORK: Multi-node mesh networks (BasicExtendedMultiSystemStream) +// +// CREATING CUSTOM PROFILES: +// To create a custom profile, add a new ProfileAlias message that specifies +// the HeaderType and PayloadType combination you want to use. +// +// CONTRIBUTING NEW HEADERS/PAYLOADS: +// If you need a new header or payload type that doesn't exist, please submit +// a PR to the struct-frame repository at https://github.com/mylonics/struct-frame package frame_formats; +// ============================================================================ +// HEADER TYPE ENUMERATION +// ============================================================================ +// HeaderType defines the framing level (start byte pattern) + +enum HeaderType { + // None: No start bytes, relies on external synchronization + HEADER_NONE = 0; + + // Tiny: 1 start byte [0x70+PayloadType] + HEADER_TINY = 1; + + // Basic: 2 start bytes [0x90] [0x70+PayloadType] + HEADER_BASIC = 2; + + // UBX: 2 start bytes [0xB5] [0x62] + HEADER_UBX = 3; + + // MavlinkV1: 1 start byte [0xFE] + HEADER_MAVLINK_V1 = 4; + + // MavlinkV2: 1 start byte [0xFD] + HEADER_MAVLINK_V2 = 5; +} + // ============================================================================ // PAYLOAD TYPE ENUMERATION // ============================================================================ // PayloadType defines how the payload is structured (header fields + CRC) -// The start byte encodes this: 0x70 + PayloadType value +// For Basic/Tiny: start byte encodes this as 0x70 + PayloadType value enum PayloadType { // Minimal: [MSG_ID] [PACKET] @@ -58,494 +90,89 @@ enum PayloadType { // ExtendedMultiSystemStream: [SEQ] [SYS_ID] [COMP_ID] [LEN16] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] // Full featured format with all extensions. PAYLOAD_EXTENDED_MULTI_SYSTEM_STREAM = 8; -} - -// ============================================================================ -// FRAME TYPE ENUMERATION -// ============================================================================ -// FrameType defines the framing level (number of start bytes) - -enum FrameType { - // None: No start bytes, relies on external synchronization - FRAME_NONE = 0; - // Tiny: 1 start byte [0x70+PayloadType] - FRAME_TINY = 1; - - // Basic: 2 start bytes [0x90] [0x70+PayloadType] - FRAME_BASIC = 2; + // Third-party protocol payloads + PAYLOAD_UBX = 100; + PAYLOAD_MAVLINK_V1 = 101; + PAYLOAD_MAVLINK_V2 = 102; } -// Legacy frame format type enumeration (for backward compatibility) -enum FrameFormatType { - // None frames (0 start bytes) - NONE_MINIMAL = 0; - NONE_DEFAULT = 1; - NONE_EXTENDED_MSG_IDS = 2; - NONE_EXTENDED_LENGTH = 3; - NONE_EXTENDED = 4; - NONE_SYS_COMP = 5; - NONE_SEQ = 6; - NONE_MULTI_SYSTEM_STREAM = 7; - NONE_EXTENDED_MULTI_SYSTEM_STREAM = 8; - - // Tiny frames (1 start byte at 0x70+PayloadType) - TINY_MINIMAL = 10; - TINY_DEFAULT = 11; - TINY_EXTENDED_MSG_IDS = 12; - TINY_EXTENDED_LENGTH = 13; - TINY_EXTENDED = 14; - TINY_SYS_COMP = 15; - TINY_SEQ = 16; - TINY_MULTI_SYSTEM_STREAM = 17; - TINY_EXTENDED_MULTI_SYSTEM_STREAM = 18; - - // Basic frames (2 start bytes: 0x90, 0x70+PayloadType) - BASIC_MINIMAL = 20; - BASIC_DEFAULT = 21; - BASIC_EXTENDED_MSG_IDS = 22; - BASIC_EXTENDED_LENGTH = 23; - BASIC_EXTENDED = 24; - BASIC_SYS_COMP = 25; - BASIC_SEQ = 26; - BASIC_MULTI_SYSTEM_STREAM = 27; - BASIC_EXTENDED_MULTI_SYSTEM_STREAM = 28; - - // Third party protocols - UBX = 100; - MAVLINK_V1 = 101; - MAVLINK_V2 = 102; -} - -// ============================================================================ -// FRAME FORMAT CONSTANTS // ============================================================================ - -// Start byte values -// Basic frame start byte 1 is always 0x90 -// Basic frame start byte 2 and Tiny frame start byte are 0x70 + PayloadType - +// STANDARD PROFILES // ============================================================================ -// NONE FRAME FORMATS (0 start bytes) -// ============================================================================ - -// NONE_MINIMAL - Minimal payload with no framing -// Format: [MSG_ID] [PACKET] -// Overhead: 1 byte -message NoneMinimal { - BasicMessage payload = 1; -} +// Pre-defined frame format profiles for common use cases -// NONE_DEFAULT - Default payload with no framing -// Format: [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 4 bytes -message NoneDefault { - uint8 length = 1; - BasicMessage payload = 2; - uint8 crc_byte1 = 3; - uint8 crc_byte2 = 4; -} +enum Profile { + // Standard profile: Basic header + Default payload + // Use case: General purpose, reliable communication (Serial/UART) + // Format: [0x90] [0x71] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] + // Overhead: 6 bytes, Max payload: 255 bytes + PROFILE_STANDARD = 0; -// NONE_EXTENDED_MSG_IDS - Extended message IDs payload with no framing -// Format: [LEN] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 5 bytes -message NoneExtendedMsgIds { - uint8 length = 1; - uint8 package_id = 2; - BasicMessage payload = 3; - uint8 crc_byte1 = 4; - uint8 crc_byte2 = 5; -} + // Sensor profile: Tiny header + Minimal payload + // Use case: Low-overhead sensor data, known message sizes (Radio/LoRa) + // Format: [0x70] [MSG_ID] [PACKET] + // Overhead: 2 bytes, No length/CRC + PROFILE_SENSOR = 1; -// NONE_EXTENDED_LENGTH - Extended length payload with no framing -// Format: [LEN_LO] [LEN_HI] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 5 bytes -message NoneExtendedLength { - uint16 length = 1; - BasicMessage payload = 2; - uint8 crc_byte1 = 3; - uint8 crc_byte2 = 4; -} + // IPC profile: None header + Minimal payload + // Use case: Trusted inter-process communication (SPI/Shared Memory) + // Format: [MSG_ID] [PACKET] + // Overhead: 1 byte, No framing + PROFILE_IPC = 2; -// NONE_EXTENDED - Extended payload with no framing -// Format: [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 6 bytes -message NoneExtended { - uint16 length = 1; - uint8 package_id = 2; - BasicMessage payload = 3; - uint8 crc_byte1 = 4; - uint8 crc_byte2 = 5; -} + // Bulk profile: Basic header + Extended payload + // Use case: Large data transfers with package namespacing (Firmware/Files) + // Format: [0x90] [0x74] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] + // Overhead: 8 bytes, Max payload: 64KB + PROFILE_BULK = 3; -// NONE_SYS_COMP - System/Component ID payload with no framing -// Format: [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 6 bytes -message NoneSysComp { - uint8 system_id = 1; - uint8 component_id = 2; - uint8 length = 3; - BasicMessage payload = 4; - uint8 crc_byte1 = 5; - uint8 crc_byte2 = 6; -} - -// NONE_SEQ - Sequence number payload with no framing -// Format: [SEQ] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 5 bytes -message NoneSeq { - uint8 sequence = 1; - uint8 length = 2; - BasicMessage payload = 3; - uint8 crc_byte1 = 4; - uint8 crc_byte2 = 5; -} - -// NONE_MULTI_SYSTEM_STREAM - Multi-system stream payload with no framing -// Format: [SEQ] [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 7 bytes -message NoneMultiSystemStream { - uint8 sequence = 1; - uint8 system_id = 2; - uint8 component_id = 3; - uint8 length = 4; - BasicMessage payload = 5; - uint8 crc_byte1 = 6; - uint8 crc_byte2 = 7; -} - -// NONE_EXTENDED_MULTI_SYSTEM_STREAM - Extended multi-system stream payload with no framing -// Format: [SEQ] [SYS_ID] [COMP_ID] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 9 bytes -message NoneExtendedMultiSystemStream { - uint8 sequence = 1; - uint8 system_id = 2; - uint8 component_id = 3; - uint16 length = 4; - uint8 package_id = 5; - BasicMessage payload = 6; - uint8 crc_byte1 = 7; - uint8 crc_byte2 = 8; + // Network profile: Basic header + ExtendedMultiSystemStream payload + // Use case: Multi-system networked communication with full features (Mesh/Swarm) + // Format: [0x90] [0x78] [SEQ] [SYS_ID] [COMP_ID] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] + // Overhead: 11 bytes, Max payload: 64KB + PROFILE_NETWORK = 4; } // ============================================================================ -// TINY FRAME FORMATS (1 start byte at 0x70+PayloadType) +// PROFILE ALIAS DEFINITIONS // ============================================================================ +// Define custom profile aliases by combining HeaderType and PayloadType -// TINY_MINIMAL - Tiny frame with minimal payload -// Format: [START=0x70] [MSG_ID] [PACKET] -// Overhead: 2 bytes -message TinyMinimal { - uint8 start_byte = 1 [(hex) = 0x70]; - BasicMessage payload = 2; +// Profile alias message - use this to define custom profiles +message ProfileAlias { + string name = 1; // Name of the profile (e.g., "MyCustomProfile") + HeaderType header_type = 2; // Header type to use + PayloadType payload_type = 3; // Payload type to use + string description = 4; // Optional description } -// TINY_DEFAULT - Tiny frame with default payload (Recommended) -// Format: [START=0x71] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 5 bytes -message TinyDefault { - uint8 start_byte = 1 [(hex) = 0x71]; - uint8 length = 2; - BasicMessage payload = 3; - uint8 crc_byte1 = 4; - uint8 crc_byte2 = 5; -} - -// TINY_EXTENDED_MSG_IDS - Tiny frame with extended message IDs -// Format: [START=0x72] [LEN] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 6 bytes -message TinyExtendedMsgIds { - uint8 start_byte = 1 [(hex) = 0x72]; - uint8 length = 2; - uint8 package_id = 3; - BasicMessage payload = 4; - uint8 crc_byte1 = 5; - uint8 crc_byte2 = 6; -} - -// TINY_EXTENDED_LENGTH - Tiny frame with extended length -// Format: [START=0x73] [LEN_LO] [LEN_HI] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 6 bytes -message TinyExtendedLength { - uint8 start_byte = 1 [(hex) = 0x73]; - uint16 length = 2; - BasicMessage payload = 3; - uint8 crc_byte1 = 4; - uint8 crc_byte2 = 5; -} +// Example custom profile aliases: -// TINY_EXTENDED - Tiny frame with extended payload -// Format: [START=0x74] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 7 bytes -message TinyExtended { - uint8 start_byte = 1 [(hex) = 0x74]; - uint16 length = 2; - uint8 package_id = 3; - BasicMessage payload = 4; - uint8 crc_byte1 = 5; - uint8 crc_byte2 = 6; -} - -// TINY_SYS_COMP - Tiny frame with system/component IDs -// Format: [START=0x75] [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 7 bytes -message TinySysComp { - uint8 start_byte = 1 [(hex) = 0x75]; - uint8 system_id = 2; - uint8 component_id = 3; - uint8 length = 4; - BasicMessage payload = 5; - uint8 crc_byte1 = 6; - uint8 crc_byte2 = 7; -} - -// TINY_SEQ - Tiny frame with sequence number -// Format: [START=0x76] [SEQ] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 6 bytes -message TinySeq { - uint8 start_byte = 1 [(hex) = 0x76]; - uint8 sequence = 2; - uint8 length = 3; - BasicMessage payload = 4; - uint8 crc_byte1 = 5; - uint8 crc_byte2 = 6; -} - -// TINY_MULTI_SYSTEM_STREAM - Tiny frame with multi-system stream -// Format: [START=0x77] [SEQ] [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 8 bytes -message TinyMultiSystemStream { - uint8 start_byte = 1 [(hex) = 0x77]; - uint8 sequence = 2; - uint8 system_id = 3; - uint8 component_id = 4; - uint8 length = 5; - BasicMessage payload = 6; - uint8 crc_byte1 = 7; - uint8 crc_byte2 = 8; -} - -// TINY_EXTENDED_MULTI_SYSTEM_STREAM - Tiny frame with extended multi-system stream -// Format: [START=0x78] [SEQ] [SYS_ID] [COMP_ID] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 10 bytes -message TinyExtendedMultiSystemStream { - uint8 start_byte = 1 [(hex) = 0x78]; - uint8 sequence = 2; - uint8 system_id = 3; - uint8 component_id = 4; - uint16 length = 5; - uint8 package_id = 6; - BasicMessage payload = 7; - uint8 crc_byte1 = 8; - uint8 crc_byte2 = 9; -} - -// ============================================================================ -// BASIC FRAME FORMATS (2 start bytes: 0x90, 0x70+PayloadType) -// ============================================================================ - -// BASIC_MINIMAL - Basic frame with minimal payload -// Format: [START1=0x90] [START2=0x70] [MSG_ID] [PACKET] -// Overhead: 3 bytes -message BasicMinimal { - uint8 start_byte1 = 1 [(hex) = 0x90]; - uint8 start_byte2 = 2 [(hex) = 0x70]; - BasicMessage payload = 3; -} - -// BASIC_DEFAULT - Basic frame with default payload (Recommended) -// Format: [START1=0x90] [START2=0x71] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 6 bytes -message BasicDefault { - uint8 start_byte1 = 1 [(hex) = 0x90]; - uint8 start_byte2 = 2 [(hex) = 0x71]; - uint8 length = 3; - BasicMessage payload = 4; - uint8 crc_byte1 = 5; - uint8 crc_byte2 = 6; -} - -// BASIC_EXTENDED_MSG_IDS - Basic frame with extended message IDs -// Format: [START1=0x90] [START2=0x72] [LEN] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 7 bytes -message BasicExtendedMsgIds { - uint8 start_byte1 = 1 [(hex) = 0x90]; - uint8 start_byte2 = 2 [(hex) = 0x72]; - uint8 length = 3; - uint8 package_id = 4; - BasicMessage payload = 5; - uint8 crc_byte1 = 6; - uint8 crc_byte2 = 7; -} - -// BASIC_EXTENDED_LENGTH - Basic frame with extended length -// Format: [START1=0x90] [START2=0x73] [LEN_LO] [LEN_HI] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 7 bytes -message BasicExtendedLength { - uint8 start_byte1 = 1 [(hex) = 0x90]; - uint8 start_byte2 = 2 [(hex) = 0x73]; - uint16 length = 3; - BasicMessage payload = 4; - uint8 crc_byte1 = 5; - uint8 crc_byte2 = 6; -} - -// BASIC_EXTENDED - Basic frame with extended payload -// Format: [START1=0x90] [START2=0x74] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 8 bytes -message BasicExtended { - uint8 start_byte1 = 1 [(hex) = 0x90]; - uint8 start_byte2 = 2 [(hex) = 0x74]; - uint16 length = 3; - uint8 package_id = 4; - BasicMessage payload = 5; - uint8 crc_byte1 = 6; - uint8 crc_byte2 = 7; -} - -// BASIC_SYS_COMP - Basic frame with system/component IDs -// Format: [START1=0x90] [START2=0x75] [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 8 bytes -message BasicSysComp { - uint8 start_byte1 = 1 [(hex) = 0x90]; - uint8 start_byte2 = 2 [(hex) = 0x75]; - uint8 system_id = 3; - uint8 component_id = 4; - uint8 length = 5; - BasicMessage payload = 6; - uint8 crc_byte1 = 7; - uint8 crc_byte2 = 8; -} - -// BASIC_SEQ - Basic frame with sequence number -// Format: [START1=0x90] [START2=0x76] [SEQ] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] +// TinyDefault - Tiny header with Default payload +// Use case: Lower overhead than Basic while still having length and CRC +// Format: [0x71] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] +// Overhead: 5 bytes +message TinyDefaultAlias { + ProfileAlias profile = 1 [default = { + name: "TinyDefault", + header_type: HEADER_TINY, + payload_type: PAYLOAD_DEFAULT, + description: "Tiny header with default payload. Lower overhead alternative to Standard profile." + }]; +} + +// BasicSeq - Basic header with Seq payload +// Use case: Need sequence numbers for packet loss detection on serial link +// Format: [0x90] [0x76] [SEQ] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] // Overhead: 7 bytes -message BasicSeq { - uint8 start_byte1 = 1 [(hex) = 0x90]; - uint8 start_byte2 = 2 [(hex) = 0x76]; - uint8 sequence = 3; - uint8 length = 4; - BasicMessage payload = 5; - uint8 crc_byte1 = 6; - uint8 crc_byte2 = 7; -} - -// BASIC_MULTI_SYSTEM_STREAM - Basic frame with multi-system stream -// Format: [START1=0x90] [START2=0x77] [SEQ] [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 9 bytes -message BasicMultiSystemStream { - uint8 start_byte1 = 1 [(hex) = 0x90]; - uint8 start_byte2 = 2 [(hex) = 0x77]; - uint8 sequence = 3; - uint8 system_id = 4; - uint8 component_id = 5; - uint8 length = 6; - BasicMessage payload = 7; - uint8 crc_byte1 = 8; - uint8 crc_byte2 = 9; -} - -// BASIC_EXTENDED_MULTI_SYSTEM_STREAM - Basic frame with extended multi-system stream -// Format: [START1=0x90] [START2=0x78] [SEQ] [SYS_ID] [COMP_ID] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Overhead: 11 bytes -message BasicExtendedMultiSystemStream { - uint8 start_byte1 = 1 [(hex) = 0x90]; - uint8 start_byte2 = 2 [(hex) = 0x78]; - uint8 sequence = 3; - uint8 system_id = 4; - uint8 component_id = 5; - uint16 length = 6; - uint8 package_id = 7; - BasicMessage payload = 8; - uint8 crc_byte1 = 9; - uint8 crc_byte2 = 10; -} - -// ============================================================================ -// THIRD PARTY PROTOCOLS -// ============================================================================ - -// UBX FRAME - u-blox GPS/GNSS protocol -// Format: [SYNC1=0xB5] [SYNC2=0x62] [CLASS] [ID] [LEN_LO] [LEN_HI] [MSG] [CK_A] [CK_B] -// Overhead: 8 bytes -message UbxFrame { - uint8 sync1 = 1 [(hex) = 0xB5]; - uint8 sync2 = 2 [(hex) = 0x62]; - uint8 msg_class = 3; - uint8 msg_id = 4; - uint8 length_lo = 5; - uint8 length_hi = 6; - uint8 ck_a = 7; - uint8 ck_b = 8; -} - -// MAVLINK V1 FRAME - Legacy drone protocol -// Format: [STX=0xFE] [LEN] [SEQ] [SYS] [COMP] [MSG] [PAYLOAD] [CRC_LO] [CRC_HI] -// Overhead: 8 bytes -message MavlinkV1Frame { - uint8 stx = 1 [(hex) = 0xFE]; - uint8 length = 2; - uint8 sequence = 3; - uint8 system_id = 4; - uint8 component_id = 5; - uint8 msg_id = 6; - uint8 crc_lo = 7; - uint8 crc_hi = 8; +message BasicSeqAlias { + ProfileAlias profile = 1 [default = { + name: "BasicSeq", + header_type: HEADER_BASIC, + payload_type: PAYLOAD_SEQ, + description: "Basic header with sequence payload. Standard profile with packet loss detection." + }]; } -// MAVLINK V2 FRAME - Modern drone protocol with extended message IDs -// Format: [STX=0xFD] [LEN] [INCOMPAT] [COMPAT] [SEQ] [SYS] [COMP] [MSG_ID x3] [PAYLOAD] [CRC] [SIG?] -// Overhead: 12-25 bytes -message MavlinkV2Frame { - uint8 stx = 1 [(hex) = 0xFD]; - uint8 length = 2; - uint8 incompat_flags = 3; - uint8 compat_flags = 4; - uint8 sequence = 5; - uint8 system_id = 6; - uint8 component_id = 7; - uint8 msg_id_0 = 8; - uint8 msg_id_1 = 9; - uint8 msg_id_2 = 10; - uint8 crc_lo = 11; - uint8 crc_hi = 12; -} - -// MAVLINK V2 SIGNATURE - Optional authentication (13 bytes) -message MavlinkV2Signature { - uint8 link_id = 1; - uint8 timestamp_0 = 2; - uint8 timestamp_1 = 3; - uint8 timestamp_2 = 4; - uint8 timestamp_3 = 5; - uint8 timestamp_4 = 6; - uint8 timestamp_5 = 7; - uint8 signature_0 = 8; - uint8 signature_1 = 9; - uint8 signature_2 = 10; - uint8 signature_3 = 11; - uint8 signature_4 = 12; - uint8 signature_5 = 13; -} - -// ============================================================================ -// HELPER MESSAGES -// ============================================================================ - -// Basic message payload included in all frame formats -message BasicMessage { - uint8 msg_id = 1; -} - -// ============================================================================ -// CONFIGURATION -// ============================================================================ - -// Runtime frame format configuration -message FrameFormatConfig { - FrameType frame_type = 1; - PayloadType payload_type = 2; - uint8 start_byte1 = 3 [(hex) = 0x90]; - uint8 start_byte2 = 4 [(hex) = 0x71]; - bool crc_enabled = 5; -} +// Users can add their own custom profile aliases here following the same pattern diff --git a/src/struct_frame/frame_formats.proto b/src/struct_frame/frame_formats.proto index 9f61bbc7..c6ede858 100644 --- a/src/struct_frame/frame_formats.proto +++ b/src/struct_frame/frame_formats.proto @@ -1,21 +1,25 @@ -// Frame Format Definitions for struct-frame -// Defines message framing formats for communication over serial, network, or byte streams. +// Frame Format Profiles for struct-frame // -// FRAMING ARCHITECTURE: -// The framing system has two levels: -// 1. HEADER (Framing Level): Determines start bytes for synchronization -// - Basic: 2 start bytes [0x90] [0x70+payload_type] -// - Tiny: 1 start byte [0x70+payload_type] -// - None: 0 start bytes (relies on external sync) -// - MavlinkV1: 1 start byte [0xFE] -// - MavlinkV2: 1 start byte [0xFD] -// - UBX: 2 start bytes [0xB5] [0x62] +// This file defines the standard frame format profiles and allows users to create +// custom profile aliases by combining HeaderType and PayloadType. // -// 2. PAYLOAD TYPE: Defines the header/footer structure of the payload -// The second start byte of Basic (or the single start byte of Tiny) encodes the payload type. +// STANDARD PROFILES: +// Instead of manually configuring headers and payloads, use these intent-based +// profiles that bundle common features for your use case: // -// Frame = Header + Payload -// Payload = PayloadHeader + Message + PayloadFooter +// - STANDARD: General purpose serial/UART (BasicDefault) +// - SENSOR: Low-bandwidth sensors (TinyMinimal) +// - IPC: Trusted inter-process communication (NoneMinimal) +// - BULK: Large file transfers (BasicExtended) +// - NETWORK: Multi-node mesh networks (BasicExtendedMultiSystemStream) +// +// CREATING CUSTOM PROFILES: +// To create a custom profile, add a new ProfileAlias message that specifies +// the HeaderType and PayloadType combination you want to use. +// +// CONTRIBUTING NEW HEADERS/PAYLOADS: +// If you need a new header or payload type that doesn't exist, please submit +// a PR to the struct-frame repository at https://github.com/mylonics/struct-frame package frame_formats; @@ -86,260 +90,89 @@ enum PayloadType { // ExtendedMultiSystemStream: [SEQ] [SYS_ID] [COMP_ID] [LEN16] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] // Full featured format with all extensions. PAYLOAD_EXTENDED_MULTI_SYSTEM_STREAM = 8; -} -// ============================================================================ -// HEADER DEFINITIONS -// ============================================================================ -// Each header type defines its start byte pattern - -// None Header - No start bytes -message HeaderNone { - // No fields - relies on external synchronization -} - -// Tiny Header - 1 start byte encoding payload type -// Start byte = 0x70 + PayloadType -message HeaderTiny { - uint8 start_byte = 1; // 0x70 + PayloadType (0x70-0x78) -} - -// Basic Header - 2 start bytes -// Start byte 1 = 0x90 (fixed) -// Start byte 2 = 0x70 + PayloadType -message HeaderBasic { - uint8 start_byte1 = 1 [(hex) = 0x90]; - uint8 start_byte2 = 2; // 0x70 + PayloadType (0x70-0x78) -} - -// UBX Header - u-blox GPS/GNSS protocol -message HeaderUbx { - uint8 sync1 = 1 [(hex) = 0xB5]; - uint8 sync2 = 2 [(hex) = 0x62]; -} - -// MavlinkV1 Header -message HeaderMavlinkV1 { - uint8 stx = 1 [(hex) = 0xFE]; -} - -// MavlinkV2 Header -message HeaderMavlinkV2 { - uint8 stx = 1 [(hex) = 0xFD]; + // Third-party protocol payloads + PAYLOAD_UBX = 100; + PAYLOAD_MAVLINK_V1 = 101; + PAYLOAD_MAVLINK_V2 = 102; } // ============================================================================ -// PAYLOAD DEFINITIONS -// ============================================================================ -// Each payload type defines its structure (fields before and after message) - -// Minimal Payload: [MSG_ID] [PACKET] -// No length field, no CRC. Requires known message sizes. -// Overhead: 1 byte -message PayloadMinimal { - uint8 msg_id = 1; - // [PACKET] - message data -} - -// Default Payload: [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// 1-byte length, 2-byte CRC. Standard format. -// Overhead: 4 bytes -message PayloadDefault { - uint8 length = 1; - uint8 msg_id = 2; - // [PACKET] - message data - uint8 crc_byte1 = 3; - uint8 crc_byte2 = 4; -} - -// ExtendedMsgIds Payload: [LEN] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Adds package ID for message namespace separation. -// Overhead: 5 bytes -message PayloadExtendedMsgIds { - uint8 length = 1; - uint8 package_id = 2; - uint8 msg_id = 3; - // [PACKET] - message data - uint8 crc_byte1 = 4; - uint8 crc_byte2 = 5; -} - -// ExtendedLength Payload: [LEN16] [MSG_ID] [PACKET] [CRC1] [CRC2] -// 2-byte length for payloads up to 65535 bytes. -// Overhead: 5 bytes -message PayloadExtendedLength { - uint16 length = 1; - uint8 msg_id = 2; - // [PACKET] - message data - uint8 crc_byte1 = 3; - uint8 crc_byte2 = 4; -} - -// Extended Payload: [LEN16] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Extended message IDs + 2-byte length. -// Overhead: 6 bytes -message PayloadExtended { - uint16 length = 1; - uint8 package_id = 2; - uint8 msg_id = 3; - // [PACKET] - message data - uint8 crc_byte1 = 4; - uint8 crc_byte2 = 5; -} - -// SysComp Payload: [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Adds system and component IDs for multi-system networks. -// Overhead: 6 bytes -message PayloadSysComp { - uint8 system_id = 1; - uint8 component_id = 2; - uint8 length = 3; - uint8 msg_id = 4; - // [PACKET] - message data - uint8 crc_byte1 = 5; - uint8 crc_byte2 = 6; -} - -// Seq Payload: [SEQ] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Adds sequence number for packet loss detection. -// Overhead: 5 bytes -message PayloadSeq { - uint8 sequence = 1; - uint8 length = 2; - uint8 msg_id = 3; - // [PACKET] - message data - uint8 crc_byte1 = 4; - uint8 crc_byte2 = 5; -} - -// MultiSystemStream Payload: [SEQ] [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Combines Seq + SysComp for multi-system streaming. -// Overhead: 7 bytes -message PayloadMultiSystemStream { - uint8 sequence = 1; - uint8 system_id = 2; - uint8 component_id = 3; - uint8 length = 4; - uint8 msg_id = 5; - // [PACKET] - message data - uint8 crc_byte1 = 6; - uint8 crc_byte2 = 7; -} - -// ExtendedMultiSystemStream Payload: [SEQ] [SYS_ID] [COMP_ID] [LEN16] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] -// Full featured format with all extensions. -// Overhead: 9 bytes -message PayloadExtendedMultiSystemStream { - uint8 sequence = 1; - uint8 system_id = 2; - uint8 component_id = 3; - uint16 length = 4; - uint8 package_id = 5; - uint8 msg_id = 6; - // [PACKET] - message data - uint8 crc_byte1 = 7; - uint8 crc_byte2 = 8; -} - -// ============================================================================ -// THIRD PARTY PROTOCOL PAYLOADS -// ============================================================================ - -// UBX Payload - u-blox GPS/GNSS protocol -// Format: [CLASS] [ID] [LEN_LO] [LEN_HI] [MSG] [CK_A] [CK_B] -// Overhead: 6 bytes (after header) -message PayloadUbx { - uint8 msg_class = 1; - uint8 msg_id = 2; - uint8 length_lo = 3; - uint8 length_hi = 4; - // [MSG] - message data - uint8 crc_byte1 = 7; - uint8 crc_byte2 = 8; -} - -// MAVLINK V1 Payload -// Format: [LEN] [SEQ] [SYS] [COMP] [MSG_ID] [PAYLOAD] [CRC_LO] [CRC_HI] -// Overhead: 7 bytes (after header) -message PayloadMavlinkV1 { - uint8 length = 1; - uint8 sequence = 2; - uint8 system_id = 3; - uint8 component_id = 4; - uint8 msg_id = 5; - // [PAYLOAD] - message data - uint8 crc_byte1 = 7; - uint8 crc_byte2 = 8; -} - -// MAVLINK V2 Payload -// Format: [LEN] [INCOMPAT] [COMPAT] [SEQ] [SYS] [COMP] [MSG_ID x3] [PAYLOAD] [CRC] [SIG?] -// Overhead: 11 bytes (after header), +13 if signed -message PayloadMavlinkV2 { - uint8 length = 1; - uint8 incompat_flags = 2; - uint8 compat_flags = 3; - uint8 sequence = 4; - uint8 system_id = 5; - uint8 component_id = 6; - uint8 msg_id_0 = 7; - uint8 msg_id_1 = 8; - uint8 msg_id_2 = 9; - // [PAYLOAD] - message data - uint8 crc_byte1 = 7; - uint8 crc_byte2 = 8; -} - - -// ============================================================================ -// RUNTIME CONFIGURATION -// ============================================================================ - -// Runtime frame format configuration -// Combines a header type with a payload type -message FrameFormatConfig { - HeaderType header_type = 1; - PayloadType payload_type = 2; - bool crc_enabled = 3; -} - -// ============================================================================ -// PROFILE DEFINITIONS +// STANDARD PROFILES // ============================================================================ // Pre-defined frame format profiles for common use cases -// Each profile combines a header type with a payload type enum Profile { // Standard profile: Basic header + Default payload - // Use case: General purpose, reliable communication + // Use case: General purpose, reliable communication (Serial/UART) // Format: [0x90] [0x71] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] + // Overhead: 6 bytes, Max payload: 255 bytes PROFILE_STANDARD = 0; // Sensor profile: Tiny header + Minimal payload - // Use case: Low-overhead sensor data, known message sizes + // Use case: Low-overhead sensor data, known message sizes (Radio/LoRa) // Format: [0x70] [MSG_ID] [PACKET] + // Overhead: 2 bytes, No length/CRC PROFILE_SENSOR = 1; + // IPC profile: None header + Minimal payload + // Use case: Trusted inter-process communication (SPI/Shared Memory) + // Format: [MSG_ID] [PACKET] + // Overhead: 1 byte, No framing + PROFILE_IPC = 2; + // Bulk profile: Basic header + Extended payload - // Use case: Large data transfers with package namespacing - // Format: [0x90] [0x74] [LEN16] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] - PROFILE_BULK = 2; + // Use case: Large data transfers with package namespacing (Firmware/Files) + // Format: [0x90] [0x74] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] + // Overhead: 8 bytes, Max payload: 64KB + PROFILE_BULK = 3; // Network profile: Basic header + ExtendedMultiSystemStream payload - // Use case: Multi-system networked communication with full features - // Format: [0x90] [0x78] [SEQ] [SYS_ID] [COMP_ID] [LEN16] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] - PROFILE_NETWORK = 3; + // Use case: Multi-system networked communication with full features (Mesh/Swarm) + // Format: [0x90] [0x78] [SEQ] [SYS_ID] [COMP_ID] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] + // Overhead: 11 bytes, Max payload: 64KB + PROFILE_NETWORK = 4; +} + +// ============================================================================ +// PROFILE ALIAS DEFINITIONS +// ============================================================================ +// Define custom profile aliases by combining HeaderType and PayloadType - // Sequenced profile: Basic header + Seq payload - // Use case: Communication requiring packet loss detection - // Format: [0x90] [0x76] [SEQ] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] - PROFILE_SEQUENCED = 4; +// Profile alias message - use this to define custom profiles +message ProfileAlias { + string name = 1; // Name of the profile (e.g., "MyCustomProfile") + HeaderType header_type = 2; // Header type to use + PayloadType payload_type = 3; // Payload type to use + string description = 4; // Optional description } -// Profile configuration mapping -message ProfileConfig { - Profile profile = 1; - HeaderType header_type = 2; - PayloadType payload_type = 3; - string description = 4; +// Example custom profile aliases: + +// TinyDefault - Tiny header with Default payload +// Use case: Lower overhead than Basic while still having length and CRC +// Format: [0x71] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] +// Overhead: 5 bytes +message TinyDefaultAlias { + ProfileAlias profile = 1 [default = { + name: "TinyDefault", + header_type: HEADER_TINY, + payload_type: PAYLOAD_DEFAULT, + description: "Tiny header with default payload. Lower overhead alternative to Standard profile." + }]; +} + +// BasicSeq - Basic header with Seq payload +// Use case: Need sequence numbers for packet loss detection on serial link +// Format: [0x90] [0x76] [SEQ] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] +// Overhead: 7 bytes +message BasicSeqAlias { + ProfileAlias profile = 1 [default = { + name: "BasicSeq", + header_type: HEADER_BASIC, + payload_type: PAYLOAD_SEQ, + description: "Basic header with sequence payload. Standard profile with packet loss detection." + }]; } + +// Users can add their own custom profile aliases here following the same pattern diff --git a/src/struct_frame/frame_formats/README.md b/src/struct_frame/frame_formats/README.md new file mode 100644 index 00000000..bdbe59fe --- /dev/null +++ b/src/struct_frame/frame_formats/README.md @@ -0,0 +1,237 @@ +# Frame Formats Module + +This module provides language-agnostic definitions for struct-frame frame formats. + +## Overview + +Frame formats define how messages are wrapped with headers, metadata fields, and error detection. The module uses a composable architecture where: + +1. **Headers** define start byte patterns for synchronization +2. **Payloads** define metadata field structure (length, CRC, etc.) +3. **Profiles** combine headers and payloads for common use cases + +## Quick Start + +### Using Standard Profiles (Recommended) + +```python +from struct_frame.frame_formats import Profile, get_profile + +# Get a standard profile +standard = get_profile(Profile.STANDARD) +print(f"Name: {standard.name}") # BasicDefault +print(f"Overhead: {standard.total_overhead} bytes") # 6 bytes +``` + +Available profiles: +- `Profile.STANDARD` - General serial/UART (BasicDefault, 6 bytes) +- `Profile.SENSOR` - Low-bandwidth sensors (TinyMinimal, 2 bytes) +- `Profile.IPC` - Trusted inter-process (NoneMinimal, 1 byte) +- `Profile.BULK` - Large file transfers (BasicExtended, 8 bytes) +- `Profile.NETWORK` - Multi-node mesh (BasicExtendedMultiSystemStream, 11 bytes) + +### Creating Custom Profiles + +```python +from struct_frame.frame_formats import create_custom_profile, HeaderType, PayloadType + +# Create a custom combination +custom = create_custom_profile( + "TinyDefault", + HeaderType.TINY, + PayloadType.DEFAULT, + "Tiny header with default payload for lower overhead" +) +print(f"Overhead: {custom.total_overhead} bytes") # 5 bytes +``` + +### Listing All Profiles + +```python +from struct_frame.frame_formats import list_profiles + +for name, format_name, overhead, description in list_profiles(): + print(f"{name}: {format_name} ({overhead} bytes)") +``` + +## Architecture + +### Headers + +Headers define the start byte pattern: + +```python +from struct_frame.frame_formats import HeaderType, get_header + +header = get_header(HeaderType.BASIC) +print(f"Start bytes: {header.num_start_bytes}") # 2 +print(f"Encodes payload: {header.encodes_payload_type}") # True +``` + +Available header types: +- `NONE` - No start bytes (0 bytes) +- `TINY` - 1 start byte encoding payload type +- `BASIC` - 2 start bytes (0x90 + payload-encoded byte) +- `UBX` - u-blox GPS protocol (0xB5, 0x62) +- `MAVLINK_V1` - Legacy drone protocol (0xFE) +- `MAVLINK_V2` - Modern drone protocol (0xFD) + +### Payloads + +Payloads define metadata field structure: + +```python +from struct_frame.frame_formats import PayloadType, get_payload + +payload = get_payload(PayloadType.DEFAULT) +print(f"Has CRC: {payload.has_crc}") # True +print(f"Has length: {payload.has_length}") # True +print(f"Overhead: {payload.overhead} bytes") # 4 bytes +``` + +Available payload types: +- `MINIMAL` - Just MSG_ID (1 byte) +- `DEFAULT` - Length + CRC (4 bytes) +- `EXTENDED_MSG_IDS` - Default + package ID (5 bytes) +- `EXTENDED_LENGTH` - 16-bit length + CRC (5 bytes) +- `EXTENDED` - Extended length + package ID (6 bytes) +- `SYS_COMP` - Routing (system/component IDs) (6 bytes) +- `SEQ` - Sequence number (5 bytes) +- `MULTI_SYSTEM_STREAM` - Routing + sequence (7 bytes) +- `EXTENDED_MULTI_SYSTEM_STREAM` - All features (9 bytes) + +### CRC Utilities + +```python +from struct_frame.frame_formats import calculate_crc16, crc_to_bytes, verify_crc + +data = b"Hello, World!" +crc = calculate_crc16(data) +crc_byte1, crc_byte2 = crc_to_bytes(crc) + +# Verify +is_valid = verify_crc(data, crc) +``` + +## File Organization + +``` +frame_formats/ +├── __init__.py # Public API +├── base.py # Core data structures +├── headers.py # Header definitions +├── payloads.py # Payload definitions +├── profiles.py # Standard profiles +└── crc.py # CRC utilities +``` + +## API Reference + +### Classes + +- `FrameFormatDefinition` - Complete frame format (header + payload) +- `HeaderDefinition` - Header specification +- `PayloadDefinition` - Payload specification + +### Enums + +- `Profile` - Standard profile enumeration +- `HeaderType` - Header type enumeration +- `PayloadType` - Payload type enumeration + +### Functions + +- `get_profile(profile: Profile)` - Get a standard profile +- `get_profile_by_name(name: str)` - Get profile by name +- `create_custom_profile(...)` - Create custom profile +- `list_profiles()` - List all standard profiles +- `get_header(header_type: HeaderType)` - Get header definition +- `get_payload(payload_type: PayloadType)` - Get payload definition +- `calculate_crc16(data: bytes)` - Calculate CRC-16-CCITT +- `crc_to_bytes(crc: int)` - Convert CRC to bytes +- `bytes_to_crc(byte1: int, byte2: int)` - Convert bytes to CRC +- `verify_crc(data: bytes, expected: int)` - Verify CRC + +## Examples + +### Exploring Profile Details + +```python +from struct_frame.frame_formats import Profile, get_profile + +network = get_profile(Profile.NETWORK) + +print(f"Name: {network.name}") +print(f"Header: {network.header.name} ({network.header.num_start_bytes} bytes)") +print(f"Payload: {network.payload.name} ({network.payload.overhead} bytes)") +print(f"Total overhead: {network.total_overhead} bytes") +print(f"Start byte: 0x{network.start_byte_value:02X}") +print(f"Has sequence: {network.payload.has_sequence}") +print(f"Has routing: {network.payload.has_system_id and network.payload.has_component_id}") +``` + +### Field Order in Payload + +```python +from struct_frame.frame_formats import get_profile, Profile + +standard = get_profile(Profile.STANDARD) +fields = standard.payload.get_field_order() +print(f"Field order: {fields}") # ['length', 'msg_id'] +``` + +### Converting to Dictionary + +```python +from struct_frame.frame_formats import get_profile, Profile + +bulk = get_profile(Profile.BULK) +data = bulk.to_dict() +print(data) +# { +# 'name': 'BasicExtended', +# 'header_type': 'BASIC', +# 'payload_type': 'EXTENDED', +# 'total_overhead': 8, +# 'max_payload_size': 65535, +# 'has_crc': True, +# 'has_sequence': False, +# 'has_routing': False, +# 'description': '...' +# } +``` + +## Testing + +Run the test suite: + +```bash +PYTHONPATH=src python3 tests/py/test_profiles.py +``` + +Or use the interactive test: + +```bash +PYTHONPATH=src python3 test_frame_formats.py +``` + +## Contributing + +To add a new header or payload type: + +1. Add enum value to `HeaderType` or `PayloadType` in `base.py` +2. Add definition to `headers.py` or `payloads.py` +3. Update the corresponding registry (`HEADER_DEFINITIONS` or `PAYLOAD_DEFINITIONS`) +4. Submit a PR to the struct-frame repository + +To add a new profile: + +1. Add enum value to `Profile` in `profiles.py` +2. Add definition to `PROFILE_DEFINITIONS` +3. Update documentation + +## See Also + +- [Framing Guide](../../docs/user-guide/framing.md) - User guide +- [Framing Architecture](../../docs/user-guide/framing-architecture.md) - Architecture details +- [Frame Profile Calculator](../../docs/user-guide/framing-calculator.md) - Interactive tool diff --git a/src/struct_frame/frame_formats/__init__.py b/src/struct_frame/frame_formats/__init__.py new file mode 100644 index 00000000..1eadd4b2 --- /dev/null +++ b/src/struct_frame/frame_formats/__init__.py @@ -0,0 +1,65 @@ +""" +Frame Format Definitions - Language-Agnostic Architecture + +This module provides language-agnostic definitions for frame formats used in struct-frame. +The definitions are organized into: + +1. Headers: Define start byte patterns for frame synchronization +2. Payloads: Define the structure of header fields and CRC +3. Profiles: Predefined combinations of headers + payloads for common use cases + +Users should use the 5 recommended profiles for most applications: +- STANDARD: General purpose serial/UART (BasicDefault) +- SENSOR: Low-bandwidth sensors (TinyMinimal) +- IPC: Trusted inter-process communication (NoneMinimal) +- BULK: Large file transfers (BasicExtended) +- NETWORK: Multi-node mesh networks (BasicExtendedMultiSystemStream) + +For custom requirements, users can create new profile aliases by combining existing +headers and payloads. New header or payload types should be contributed via PR. +""" + +from .base import ( + HeaderType, + PayloadType, + FrameFormatDefinition, + HeaderDefinition, + PayloadDefinition, +) +from .profiles import ( + Profile, + PROFILE_DEFINITIONS, + get_profile, + get_profile_by_name, + create_custom_profile, + list_profiles, +) +from .headers import HEADER_DEFINITIONS, get_header +from .payloads import PAYLOAD_DEFINITIONS, get_payload +from .crc import calculate_crc16, crc_to_bytes, bytes_to_crc, verify_crc + +__all__ = [ + # Base types + 'HeaderType', + 'PayloadType', + 'FrameFormatDefinition', + 'HeaderDefinition', + 'PayloadDefinition', + # Profiles + 'Profile', + 'PROFILE_DEFINITIONS', + 'get_profile', + 'get_profile_by_name', + 'create_custom_profile', + 'list_profiles', + # Headers and Payloads + 'HEADER_DEFINITIONS', + 'PAYLOAD_DEFINITIONS', + 'get_header', + 'get_payload', + # CRC utilities + 'calculate_crc16', + 'crc_to_bytes', + 'bytes_to_crc', + 'verify_crc', +] diff --git a/src/struct_frame/frame_formats/base.py b/src/struct_frame/frame_formats/base.py new file mode 100644 index 00000000..946a891b --- /dev/null +++ b/src/struct_frame/frame_formats/base.py @@ -0,0 +1,221 @@ +""" +Base classes and enumerations for frame formats. + +This module defines the core data structures used across all frame format definitions. +""" + +from enum import Enum +from dataclasses import dataclass, field +from typing import List, Optional, Dict, Any + + +class HeaderType(Enum): + """ + Header types define the start byte pattern used for frame synchronization. + + The header determines how many start bytes precede the payload and whether + the payload type is encoded in the start bytes. + """ + NONE = 0 # No start bytes (relies on external synchronization) + TINY = 1 # 1 start byte [0x70+PayloadType] + BASIC = 2 # 2 start bytes [0x90] [0x70+PayloadType] + UBX = 3 # 2 start bytes [0xB5] [0x62] (u-blox GPS) + MAVLINK_V1 = 4 # 1 start byte [0xFE] (Legacy drone protocol) + MAVLINK_V2 = 5 # 1 start byte [0xFD] (Modern drone protocol) + + +class PayloadType(Enum): + """ + Payload types define the structure of header fields and footer (CRC) after start bytes. + + The payload type determines what metadata fields are included (length, sequence, + system ID, etc.) and whether CRC error detection is used. + """ + MINIMAL = 0 # [MSG_ID] [PACKET] + DEFAULT = 1 # [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] + EXTENDED_MSG_IDS = 2 # [LEN] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] + EXTENDED_LENGTH = 3 # [LEN16] [MSG_ID] [PACKET] [CRC1] [CRC2] + EXTENDED = 4 # [LEN16] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] + SYS_COMP = 5 # [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] + SEQ = 6 # [SEQ] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] + MULTI_SYSTEM_STREAM = 7 # [SEQ] [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] + EXTENDED_MULTI_SYSTEM_STREAM = 8 # [SEQ] [SYS_ID] [COMP_ID] [LEN16] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] + # Third-party protocol payloads (value >= 100) + UBX = 100 # [CLASS] [ID] [LEN_LO] [LEN_HI] [MSG] [CK_A] [CK_B] + MAVLINK_V1 = 101 # [LEN] [SEQ] [SYS] [COMP] [MSG_ID] [PAYLOAD] [CRC_LO] [CRC_HI] + MAVLINK_V2 = 102 # [LEN] [INCOMPAT] [COMPAT] [SEQ] [SYS] [COMP] [MSG_ID x3] [PAYLOAD] [CRC] + + +@dataclass +class HeaderDefinition: + """ + Defines a header type's structure and behavior. + + Attributes: + header_type: The type of header + name: Human-readable name + start_bytes: Fixed start byte values (empty list for variable) + num_start_bytes: Total number of start bytes + encodes_payload_type: True if start byte encodes payload type (Basic, Tiny) + payload_type_byte_index: Index of byte that encodes payload type (-1 if none) + description: Human-readable description + """ + header_type: HeaderType + name: str + start_bytes: List[int] = field(default_factory=list) + num_start_bytes: int = 0 + encodes_payload_type: bool = False + payload_type_byte_index: int = -1 + description: str = "" + + @property + def size(self) -> int: + """Size of header in bytes""" + return self.num_start_bytes + + @property + def is_fixed(self) -> bool: + """True if all start bytes have fixed values""" + return len(self.start_bytes) == self.num_start_bytes and not self.encodes_payload_type + + +@dataclass +class PayloadDefinition: + """ + Defines a payload type's structure and metadata fields. + + Attributes: + payload_type: The type of payload + name: Human-readable name + has_crc: True if payload includes CRC error detection + crc_bytes: Number of CRC bytes (typically 0 or 2) + has_length: True if payload includes a length field + length_bytes: Size of length field (1 for uint8, 2 for uint16) + has_sequence: True if payload includes a sequence number + has_system_id: True if payload includes a system ID + has_component_id: True if payload includes a component ID + has_package_id: True if payload includes a package ID + description: Human-readable description + """ + payload_type: PayloadType + name: str + has_crc: bool = False + crc_bytes: int = 0 + has_length: bool = False + length_bytes: int = 0 + has_sequence: bool = False + has_system_id: bool = False + has_component_id: bool = False + has_package_id: bool = False + description: str = "" + + @property + def header_size(self) -> int: + """Size of payload header (fields before message data) in bytes""" + size = 1 # msg_id is always 1 byte + if self.has_length: + size += self.length_bytes + if self.has_sequence: + size += 1 + if self.has_system_id: + size += 1 + if self.has_component_id: + size += 1 + if self.has_package_id: + size += 1 + return size + + @property + def footer_size(self) -> int: + """Size of payload footer (CRC bytes after message data)""" + return self.crc_bytes + + @property + def overhead(self) -> int: + """Total payload overhead (header + footer) in bytes""" + return self.header_size + self.footer_size + + def get_field_order(self) -> List[str]: + """ + Get the order of fields in the payload header. + + Returns: + List of field names in the order they appear in the frame + """ + fields = [] + if self.has_sequence: + fields.append('sequence') + if self.has_system_id: + fields.append('system_id') + if self.has_component_id: + fields.append('component_id') + if self.has_length: + if self.length_bytes == 2: + fields.append('length_lo') + fields.append('length_hi') + else: + fields.append('length') + if self.has_package_id: + fields.append('package_id') + fields.append('msg_id') + return fields + + +@dataclass +class FrameFormatDefinition: + """ + Complete frame format definition combining header and payload. + + A frame format is the complete specification of how to encode/decode messages, + including both the framing (start bytes) and payload structure. + + Attributes: + name: Human-readable name (e.g., "BasicDefault") + header: Header definition + payload: Payload definition + description: Human-readable description + """ + name: str + header: HeaderDefinition + payload: PayloadDefinition + description: str = "" + + @property + def total_overhead(self) -> int: + """Total overhead (header + payload header + payload footer) in bytes""" + return self.header.size + self.payload.overhead + + @property + def start_byte_value(self) -> Optional[int]: + """ + Get the start byte value (for Tiny/Basic with encoded payload type). + + Returns: + The computed start byte value, or None if not applicable + """ + if self.header.encodes_payload_type: + return 0x70 + self.payload.payload_type.value + return None + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary representation""" + return { + 'name': self.name, + 'header_type': self.header.header_type.name, + 'payload_type': self.payload.payload_type.name, + 'total_overhead': self.total_overhead, + 'max_payload_size': 65535 if self.payload.length_bytes == 2 else 255 if self.payload.has_length else None, + 'has_crc': self.payload.has_crc, + 'has_sequence': self.payload.has_sequence, + 'has_routing': self.payload.has_system_id and self.payload.has_component_id, + 'description': self.description, + } + + +# Constants +BASIC_START_BYTE = 0x90 +PAYLOAD_TYPE_BASE = 0x70 +UBX_SYNC1 = 0xB5 +UBX_SYNC2 = 0x62 +MAVLINK_V1_STX = 0xFE +MAVLINK_V2_STX = 0xFD diff --git a/src/struct_frame/frame_formats/crc.py b/src/struct_frame/frame_formats/crc.py new file mode 100644 index 00000000..693d75f8 --- /dev/null +++ b/src/struct_frame/frame_formats/crc.py @@ -0,0 +1,76 @@ +""" +CRC calculation utilities for frame formats. + +Provides CRC-16-CCITT implementation used by struct-frame payloads. +""" + + +def calculate_crc16(data: bytes) -> int: + """ + Calculate CRC-16-CCITT checksum for data. + + Polynomial: 0x1021 (x^16 + x^12 + x^5 + 1) + Initial value: 0xFFFF + + Args: + data: Bytes to calculate CRC for + + Returns: + 16-bit CRC value + """ + crc = 0xFFFF + + for byte in data: + crc ^= (byte << 8) + for _ in range(8): + if crc & 0x8000: + crc = (crc << 1) ^ 0x1021 + else: + crc = crc << 1 + crc &= 0xFFFF + + return crc + + +def crc_to_bytes(crc: int) -> tuple: + """ + Convert 16-bit CRC to two bytes. + + Args: + crc: 16-bit CRC value + + Returns: + Tuple of (crc_byte1, crc_byte2) in little-endian order + """ + crc_byte1 = crc & 0xFF + crc_byte2 = (crc >> 8) & 0xFF + return (crc_byte1, crc_byte2) + + +def bytes_to_crc(crc_byte1: int, crc_byte2: int) -> int: + """ + Convert two bytes to 16-bit CRC value. + + Args: + crc_byte1: Low byte of CRC + crc_byte2: High byte of CRC + + Returns: + 16-bit CRC value + """ + return crc_byte1 | (crc_byte2 << 8) + + +def verify_crc(data: bytes, expected_crc: int) -> bool: + """ + Verify CRC checksum matches expected value. + + Args: + data: Data to verify + expected_crc: Expected CRC value + + Returns: + True if CRC matches, False otherwise + """ + calculated_crc = calculate_crc16(data) + return calculated_crc == expected_crc diff --git a/src/struct_frame/frame_formats/headers.py b/src/struct_frame/frame_formats/headers.py new file mode 100644 index 00000000..5b4d2b95 --- /dev/null +++ b/src/struct_frame/frame_formats/headers.py @@ -0,0 +1,103 @@ +""" +Header definitions for frame formats. + +Each header defines the start byte pattern used for frame synchronization. +""" + +from .base import HeaderType, HeaderDefinition, BASIC_START_BYTE, PAYLOAD_TYPE_BASE +from .base import UBX_SYNC1, UBX_SYNC2, MAVLINK_V1_STX, MAVLINK_V2_STX + + +# Header None - No start bytes +# Relies on external synchronization (e.g., message length from lower layer) +HEADER_NONE = HeaderDefinition( + header_type=HeaderType.NONE, + name="None", + start_bytes=[], + num_start_bytes=0, + encodes_payload_type=False, + payload_type_byte_index=-1, + description="No start bytes. Relies on external synchronization." +) + + +# Header Tiny - 1 start byte encoding payload type +# Start byte = 0x70 + PayloadType value +HEADER_TINY = HeaderDefinition( + header_type=HeaderType.TINY, + name="Tiny", + start_bytes=[], # Variable based on payload type + num_start_bytes=1, + encodes_payload_type=True, + payload_type_byte_index=0, + description="1 start byte [0x70+PayloadType]. Minimal overhead." +) + + +# Header Basic - 2 start bytes +# Start byte 1 = 0x90 (fixed) +# Start byte 2 = 0x70 + PayloadType value +HEADER_BASIC = HeaderDefinition( + header_type=HeaderType.BASIC, + name="Basic", + start_bytes=[BASIC_START_BYTE], # First byte is fixed, second varies + num_start_bytes=2, + encodes_payload_type=True, + payload_type_byte_index=1, + description="2 start bytes [0x90] [0x70+PayloadType]. Standard framing." +) + + +# Header UBX - u-blox GPS/GNSS protocol +# 2 fixed start bytes: 0xB5, 0x62 +HEADER_UBX = HeaderDefinition( + header_type=HeaderType.UBX, + name="UBX", + start_bytes=[UBX_SYNC1, UBX_SYNC2], + num_start_bytes=2, + encodes_payload_type=False, + payload_type_byte_index=-1, + description="u-blox GPS/GNSS protocol. 2 start bytes [0xB5] [0x62]." +) + + +# Header MAVLink V1 - Legacy drone protocol +# 1 fixed start byte: 0xFE +HEADER_MAVLINK_V1 = HeaderDefinition( + header_type=HeaderType.MAVLINK_V1, + name="MavlinkV1", + start_bytes=[MAVLINK_V1_STX], + num_start_bytes=1, + encodes_payload_type=False, + payload_type_byte_index=-1, + description="MAVLink V1 protocol. 1 start byte [0xFE]." +) + + +# Header MAVLink V2 - Modern drone protocol +# 1 fixed start byte: 0xFD +HEADER_MAVLINK_V2 = HeaderDefinition( + header_type=HeaderType.MAVLINK_V2, + name="MavlinkV2", + start_bytes=[MAVLINK_V2_STX], + num_start_bytes=1, + encodes_payload_type=False, + payload_type_byte_index=-1, + description="MAVLink V2 protocol. 1 start byte [0xFD]." +) + + +# Registry of all header definitions +HEADER_DEFINITIONS = { + HeaderType.NONE: HEADER_NONE, + HeaderType.TINY: HEADER_TINY, + HeaderType.BASIC: HEADER_BASIC, + HeaderType.UBX: HEADER_UBX, + HeaderType.MAVLINK_V1: HEADER_MAVLINK_V1, + HeaderType.MAVLINK_V2: HEADER_MAVLINK_V2, +} + + +def get_header(header_type: HeaderType) -> HeaderDefinition: + """Get header definition by type""" + return HEADER_DEFINITIONS[header_type] diff --git a/src/struct_frame/frame_formats/payloads.py b/src/struct_frame/frame_formats/payloads.py new file mode 100644 index 00000000..0e1fc62c --- /dev/null +++ b/src/struct_frame/frame_formats/payloads.py @@ -0,0 +1,249 @@ +""" +Payload definitions for frame formats. + +Each payload defines the structure of header fields (before message data) +and footer fields (after message data, typically CRC). + +Payload definitions are designed to be composable - more complex payloads +can build upon simpler ones by adding fields. +""" + +from .base import PayloadType, PayloadDefinition + + +# Payload Minimal +# Format: [MSG_ID] [PACKET] +# No length field, no CRC. Requires known message sizes. +# This is the base - all other payloads add to this +PAYLOAD_MINIMAL = PayloadDefinition( + payload_type=PayloadType.MINIMAL, + name="Minimal", + has_crc=False, + crc_bytes=0, + has_length=False, + length_bytes=0, + has_sequence=False, + has_system_id=False, + has_component_id=False, + has_package_id=False, + description="[MSG_ID] [PACKET] - Minimal overhead. No length, no CRC." +) + + +# Payload Default +# Format: [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] +# Builds on Minimal by adding: 1-byte length field + 2-byte CRC +PAYLOAD_DEFAULT = PayloadDefinition( + payload_type=PayloadType.DEFAULT, + name="Default", + has_crc=True, + crc_bytes=2, + has_length=True, + length_bytes=1, + has_sequence=False, + has_system_id=False, + has_component_id=False, + has_package_id=False, + description="[LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] - Standard format with length and CRC." +) + + +# Payload ExtendedMsgIds +# Format: [LEN] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] +# Builds on Default by adding: package ID for namespace separation +PAYLOAD_EXTENDED_MSG_IDS = PayloadDefinition( + payload_type=PayloadType.EXTENDED_MSG_IDS, + name="ExtendedMsgIds", + has_crc=True, + crc_bytes=2, + has_length=True, + length_bytes=1, + has_sequence=False, + has_system_id=False, + has_component_id=False, + has_package_id=True, + description="[LEN] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] - Adds package namespace." +) + + +# Payload ExtendedLength +# Format: [LEN16] [MSG_ID] [PACKET] [CRC1] [CRC2] +# Builds on Default by upgrading: 1-byte length -> 2-byte length (up to 64KB) +PAYLOAD_EXTENDED_LENGTH = PayloadDefinition( + payload_type=PayloadType.EXTENDED_LENGTH, + name="ExtendedLength", + has_crc=True, + crc_bytes=2, + has_length=True, + length_bytes=2, + has_sequence=False, + has_system_id=False, + has_component_id=False, + has_package_id=False, + description="[LEN16] [MSG_ID] [PACKET] [CRC1] [CRC2] - Supports payloads up to 64KB." +) + + +# Payload Extended +# Format: [LEN16] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] +# Combines ExtendedLength + ExtendedMsgIds: 2-byte length + package ID +PAYLOAD_EXTENDED = PayloadDefinition( + payload_type=PayloadType.EXTENDED, + name="Extended", + has_crc=True, + crc_bytes=2, + has_length=True, + length_bytes=2, + has_sequence=False, + has_system_id=False, + has_component_id=False, + has_package_id=True, + description="[LEN16] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] - Large payloads with package namespace." +) + + +# Payload SysComp +# Format: [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] +# Builds on Default by adding: system ID + component ID for multi-node routing +PAYLOAD_SYS_COMP = PayloadDefinition( + payload_type=PayloadType.SYS_COMP, + name="SysComp", + has_crc=True, + crc_bytes=2, + has_length=True, + length_bytes=1, + has_sequence=False, + has_system_id=True, + has_component_id=True, + has_package_id=False, + description="[SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] - Multi-node routing." +) + + +# Payload Seq +# Format: [SEQ] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] +# Builds on Default by adding: sequence number for packet loss detection +PAYLOAD_SEQ = PayloadDefinition( + payload_type=PayloadType.SEQ, + name="Seq", + has_crc=True, + crc_bytes=2, + has_length=True, + length_bytes=1, + has_sequence=True, + has_system_id=False, + has_component_id=False, + has_package_id=False, + description="[SEQ] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] - Adds sequence for packet loss detection." +) + + +# Payload MultiSystemStream +# Format: [SEQ] [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] +# Combines Seq + SysComp: sequence + routing for multi-system streaming +PAYLOAD_MULTI_SYSTEM_STREAM = PayloadDefinition( + payload_type=PayloadType.MULTI_SYSTEM_STREAM, + name="MultiSystemStream", + has_crc=True, + crc_bytes=2, + has_length=True, + length_bytes=1, + has_sequence=True, + has_system_id=True, + has_component_id=True, + has_package_id=False, + description="[SEQ] [SYS_ID] [COMP_ID] [LEN] [MSG_ID] [PACKET] [CRC1] [CRC2] - Multi-system streaming." +) + + +# Payload ExtendedMultiSystemStream +# Format: [SEQ] [SYS_ID] [COMP_ID] [LEN16] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] +# Full-featured: sequence + routing + 2-byte length + package ID +PAYLOAD_EXTENDED_MULTI_SYSTEM_STREAM = PayloadDefinition( + payload_type=PayloadType.EXTENDED_MULTI_SYSTEM_STREAM, + name="ExtendedMultiSystemStream", + has_crc=True, + crc_bytes=2, + has_length=True, + length_bytes=2, + has_sequence=True, + has_system_id=True, + has_component_id=True, + has_package_id=True, + description="[SEQ] [SYS_ID] [COMP_ID] [LEN16] [PKG_ID] [MSG_ID] [PACKET] [CRC1] [CRC2] - Full featured." +) + + +# Third-party protocol payloads + +# Payload UBX - u-blox GPS/GNSS protocol +# Format: [CLASS] [ID] [LEN_LO] [LEN_HI] [MSG] [CK_A] [CK_B] +PAYLOAD_UBX = PayloadDefinition( + payload_type=PayloadType.UBX, + name="UBX", + has_crc=True, + crc_bytes=2, # UBX uses 2-byte checksum + has_length=True, + length_bytes=2, + has_sequence=False, + has_system_id=False, + has_component_id=False, + has_package_id=False, + description="[CLASS] [ID] [LEN_LO] [LEN_HI] [MSG] [CK_A] [CK_B] - u-blox GPS protocol." +) + + +# Payload MAVLink V1 +# Format: [LEN] [SEQ] [SYS] [COMP] [MSG_ID] [PAYLOAD] [CRC_LO] [CRC_HI] +PAYLOAD_MAVLINK_V1 = PayloadDefinition( + payload_type=PayloadType.MAVLINK_V1, + name="MavlinkV1", + has_crc=True, + crc_bytes=2, + has_length=True, + length_bytes=1, + has_sequence=True, + has_system_id=True, + has_component_id=True, + has_package_id=False, + description="[LEN] [SEQ] [SYS] [COMP] [MSG_ID] [PAYLOAD] [CRC_LO] [CRC_HI] - MAVLink V1." +) + + +# Payload MAVLink V2 +# Format: [LEN] [INCOMPAT] [COMPAT] [SEQ] [SYS] [COMP] [MSG_ID x3] [PAYLOAD] [CRC] [SIG?] +PAYLOAD_MAVLINK_V2 = PayloadDefinition( + payload_type=PayloadType.MAVLINK_V2, + name="MavlinkV2", + has_crc=True, + crc_bytes=2, # Note: optional 13-byte signature not counted here + has_length=True, + length_bytes=1, + has_sequence=True, + has_system_id=True, + has_component_id=True, + has_package_id=False, + description="[LEN] [INCOMPAT] [COMPAT] [SEQ] [SYS] [COMP] [MSG_ID x3] [PAYLOAD] [CRC] - MAVLink V2." +) + + +# Registry of all payload definitions +PAYLOAD_DEFINITIONS = { + PayloadType.MINIMAL: PAYLOAD_MINIMAL, + PayloadType.DEFAULT: PAYLOAD_DEFAULT, + PayloadType.EXTENDED_MSG_IDS: PAYLOAD_EXTENDED_MSG_IDS, + PayloadType.EXTENDED_LENGTH: PAYLOAD_EXTENDED_LENGTH, + PayloadType.EXTENDED: PAYLOAD_EXTENDED, + PayloadType.SYS_COMP: PAYLOAD_SYS_COMP, + PayloadType.SEQ: PAYLOAD_SEQ, + PayloadType.MULTI_SYSTEM_STREAM: PAYLOAD_MULTI_SYSTEM_STREAM, + PayloadType.EXTENDED_MULTI_SYSTEM_STREAM: PAYLOAD_EXTENDED_MULTI_SYSTEM_STREAM, + PayloadType.UBX: PAYLOAD_UBX, + PayloadType.MAVLINK_V1: PAYLOAD_MAVLINK_V1, + PayloadType.MAVLINK_V2: PAYLOAD_MAVLINK_V2, +} + + +def get_payload(payload_type: PayloadType) -> PayloadDefinition: + """Get payload definition by type""" + return PAYLOAD_DEFINITIONS[payload_type] diff --git a/src/struct_frame/frame_formats/profiles.py b/src/struct_frame/frame_formats/profiles.py new file mode 100644 index 00000000..c6d6ae48 --- /dev/null +++ b/src/struct_frame/frame_formats/profiles.py @@ -0,0 +1,198 @@ +""" +Profile definitions for frame formats. + +Profiles are predefined combinations of headers and payloads for common use cases. +Users should use these 5 recommended profiles for most applications. + +To create a custom profile, users can define it in their frame_format.proto file +by specifying a profile alias that combines a HeaderType and PayloadType. + +For new header or payload types, please submit a PR to the struct-frame repository. +""" + +from enum import Enum +from .base import HeaderType, PayloadType, FrameFormatDefinition +from .headers import get_header +from .payloads import get_payload + + +class Profile(Enum): + """ + Standard frame format profiles for common use cases. + + These profiles provide intent-based frame format selection rather than + requiring users to understand the technical details of headers and payloads. + """ + # Standard profile: Basic header + Default payload + # Use case: General purpose reliable communication (Serial/UART) + STANDARD = 0 + + # Sensor profile: Tiny header + Minimal payload + # Use case: Low-overhead sensor data with known message sizes (Radio/LoRa) + SENSOR = 1 + + # IPC profile: None header + Minimal payload + # Use case: Trusted inter-process communication (SPI/Shared Memory) + IPC = 2 + + # Bulk profile: Basic header + Extended payload + # Use case: Large data transfers with package namespacing (Firmware/Files) + BULK = 3 + + # Network profile: Basic header + ExtendedMultiSystemStream payload + # Use case: Multi-system networked communication (Mesh/Swarm) + NETWORK = 4 + + +def _create_profile_definition( + profile: Profile, + header_type: HeaderType, + payload_type: PayloadType, + description: str +) -> FrameFormatDefinition: + """Create a frame format definition for a profile""" + header = get_header(header_type) + payload = get_payload(payload_type) + + # Generate name from header and payload (e.g., "BasicDefault") + name = f"{header.name}{payload.name}" + + return FrameFormatDefinition( + name=name, + header=header, + payload=payload, + description=description + ) + + +# Define the 5 standard profiles +PROFILE_DEFINITIONS = { + Profile.STANDARD: _create_profile_definition( + Profile.STANDARD, + HeaderType.BASIC, + PayloadType.DEFAULT, + "General purpose serial/UART communication. Basic header with default payload. " + "Frame: [0x90] [0x71] [LEN] [MSG_ID] [PAYLOAD] [CRC1] [CRC2]. " + "Overhead: 6 bytes. Max payload: 255 bytes." + ), + + Profile.SENSOR: _create_profile_definition( + Profile.SENSOR, + HeaderType.TINY, + PayloadType.MINIMAL, + "Low-bandwidth sensor data with known message sizes. Tiny header with minimal payload. " + "Frame: [0x70] [MSG_ID] [PAYLOAD]. " + "Overhead: 2 bytes. No length field or CRC. Requires fixed message sizes." + ), + + Profile.IPC: _create_profile_definition( + Profile.IPC, + HeaderType.NONE, + PayloadType.MINIMAL, + "Trusted inter-process communication. No header, minimal payload. " + "Frame: [MSG_ID] [PAYLOAD]. " + "Overhead: 1 byte. Relies on external synchronization (e.g., message framing from transport layer)." + ), + + Profile.BULK: _create_profile_definition( + Profile.BULK, + HeaderType.BASIC, + PayloadType.EXTENDED, + "Large data transfers with package namespacing. Basic header with extended payload. " + "Frame: [0x90] [0x74] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PAYLOAD] [CRC1] [CRC2]. " + "Overhead: 8 bytes. Max payload: 64KB. Supports package namespaces." + ), + + Profile.NETWORK: _create_profile_definition( + Profile.NETWORK, + HeaderType.BASIC, + PayloadType.EXTENDED_MULTI_SYSTEM_STREAM, + "Multi-node networked communication. Basic header with full-featured payload. " + "Frame: [0x90] [0x78] [SEQ] [SYS_ID] [COMP_ID] [LEN_LO] [LEN_HI] [PKG_ID] [MSG_ID] [PAYLOAD] [CRC1] [CRC2]. " + "Overhead: 11 bytes. Max payload: 64KB. Supports routing, sequencing, and package namespaces." + ), +} + + +def get_profile(profile: Profile) -> FrameFormatDefinition: + """ + Get frame format definition for a standard profile. + + Args: + profile: The profile to get + + Returns: + FrameFormatDefinition for the profile + """ + return PROFILE_DEFINITIONS[profile] + + +def get_profile_by_name(name: str) -> FrameFormatDefinition: + """ + Get frame format definition by profile name. + + Args: + name: Profile name (e.g., "STANDARD", "SENSOR", etc.) + + Returns: + FrameFormatDefinition for the profile + + Raises: + ValueError: If profile name is not recognized + """ + try: + profile = Profile[name.upper()] + return get_profile(profile) + except KeyError: + raise ValueError(f"Unknown profile: {name}. Valid profiles: {[p.name for p in Profile]}") + + +def create_custom_profile( + name: str, + header_type: HeaderType, + payload_type: PayloadType, + description: str = "" +) -> FrameFormatDefinition: + """ + Create a custom frame format profile. + + This allows users to create custom combinations of headers and payloads + without modifying the struct-frame codebase. + + Args: + name: Name for the custom profile + header_type: Header type to use + payload_type: Payload type to use + description: Optional description + + Returns: + FrameFormatDefinition for the custom profile + """ + header = get_header(header_type) + payload = get_payload(payload_type) + + return FrameFormatDefinition( + name=name, + header=header, + payload=payload, + description=description or f"Custom profile: {header.name} + {payload.name}" + ) + + +def list_profiles(): + """ + List all standard profiles with their details. + + Returns: + List of tuples: (profile_name, frame_format_name, description) + """ + profiles = [] + for profile in Profile: + definition = get_profile(profile) + profiles.append(( + profile.name, + definition.name, + definition.total_overhead, + definition.description + )) + return profiles diff --git a/tests/py/test_profiles.py b/tests/py/test_profiles.py new file mode 100644 index 00000000..d4dc915b --- /dev/null +++ b/tests/py/test_profiles.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" +Test the 5 standard frame format profiles across all languages. + +This test validates that the 5 recommended profiles (STANDARD, SENSOR, IPC, BULK, NETWORK) +work correctly for code generation, serialization, and deserialization across all supported +languages. +""" + +import sys +import os + +# Add src to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src')) + +from struct_frame.frame_formats import ( + Profile, + get_profile, + HeaderType, + PayloadType, +) + + +def test_profile_definitions(): + """Test that all 5 standard profiles are correctly defined""" + print("\n[TEST] Profile Definitions") + + # Test STANDARD profile + standard = get_profile(Profile.STANDARD) + assert standard.name == "BasicDefault" + assert standard.header.header_type == HeaderType.BASIC + assert standard.payload.payload_type == PayloadType.DEFAULT + assert standard.total_overhead == 6 + assert standard.start_byte_value == 0x71 + print(f" ✓ Profile.STANDARD = {standard.name} (overhead: {standard.total_overhead} bytes)") + + # Test SENSOR profile + sensor = get_profile(Profile.SENSOR) + assert sensor.name == "TinyMinimal" + assert sensor.header.header_type == HeaderType.TINY + assert sensor.payload.payload_type == PayloadType.MINIMAL + assert sensor.total_overhead == 2 + assert sensor.start_byte_value == 0x70 + print(f" ✓ Profile.SENSOR = {sensor.name} (overhead: {sensor.total_overhead} bytes)") + + # Test IPC profile + ipc = get_profile(Profile.IPC) + assert ipc.name == "NoneMinimal" + assert ipc.header.header_type == HeaderType.NONE + assert ipc.payload.payload_type == PayloadType.MINIMAL + assert ipc.total_overhead == 1 + assert ipc.start_byte_value is None # No start bytes + print(f" ✓ Profile.IPC = {ipc.name} (overhead: {ipc.total_overhead} bytes)") + + # Test BULK profile + bulk = get_profile(Profile.BULK) + assert bulk.name == "BasicExtended" + assert bulk.header.header_type == HeaderType.BASIC + assert bulk.payload.payload_type == PayloadType.EXTENDED + assert bulk.total_overhead == 8 + assert bulk.start_byte_value == 0x74 + print(f" ✓ Profile.BULK = {bulk.name} (overhead: {bulk.total_overhead} bytes)") + + # Test NETWORK profile + network = get_profile(Profile.NETWORK) + assert network.name == "BasicExtendedMultiSystemStream" + assert network.header.header_type == HeaderType.BASIC + assert network.payload.payload_type == PayloadType.EXTENDED_MULTI_SYSTEM_STREAM + assert network.total_overhead == 11 + assert network.start_byte_value == 0x78 + print(f" ✓ Profile.NETWORK = {network.name} (overhead: {network.total_overhead} bytes)") + + print(" PASS: All 5 standard profiles correctly defined") + + +def test_profile_features(): + """Test that profiles have the expected features""" + print("\n[TEST] Profile Features") + + # STANDARD should have length and CRC + standard = get_profile(Profile.STANDARD) + assert standard.payload.has_length == True + assert standard.payload.has_crc == True + assert standard.payload.has_sequence == False + assert standard.payload.has_system_id == False + print(f" ✓ STANDARD has length and CRC") + + # SENSOR should have minimal features + sensor = get_profile(Profile.SENSOR) + assert sensor.payload.has_length == False + assert sensor.payload.has_crc == False + assert sensor.payload.has_sequence == False + print(f" ✓ SENSOR has minimal features (no length, no CRC)") + + # IPC should have minimal features + ipc = get_profile(Profile.IPC) + assert ipc.payload.has_length == False + assert ipc.payload.has_crc == False + print(f" ✓ IPC has minimal features") + + # BULK should have extended length and package ID + bulk = get_profile(Profile.BULK) + assert bulk.payload.has_length == True + assert bulk.payload.length_bytes == 2 # 16-bit length + assert bulk.payload.has_package_id == True + assert bulk.payload.has_crc == True + print(f" ✓ BULK has 16-bit length and package ID") + + # NETWORK should have all features + network = get_profile(Profile.NETWORK) + assert network.payload.has_length == True + assert network.payload.length_bytes == 2 # 16-bit length + assert network.payload.has_sequence == True + assert network.payload.has_system_id == True + assert network.payload.has_component_id == True + assert network.payload.has_package_id == True + assert network.payload.has_crc == True + print(f" ✓ NETWORK has all features (seq, routing, 16-bit length, package ID, CRC)") + + print(" PASS: All profiles have expected features") + + +def test_profile_payload_sizes(): + """Test that profiles support expected payload sizes""" + print("\n[TEST] Profile Payload Size Limits") + + # STANDARD: 255 bytes max (1-byte length) + standard = get_profile(Profile.STANDARD) + max_payload = 255 if standard.payload.length_bytes == 1 else 65535 + assert max_payload == 255 + print(f" ✓ STANDARD: max {max_payload} bytes") + + # SENSOR: No length field (requires known message sizes) + sensor = get_profile(Profile.SENSOR) + assert sensor.payload.has_length == False + print(f" ✓ SENSOR: no length field (fixed-size messages)") + + # IPC: No length field + ipc = get_profile(Profile.IPC) + assert ipc.payload.has_length == False + print(f" ✓ IPC: no length field") + + # BULK: 65535 bytes max (2-byte length) + bulk = get_profile(Profile.BULK) + max_payload = 65535 if bulk.payload.length_bytes == 2 else 255 + assert max_payload == 65535 + print(f" ✓ BULK: max {max_payload} bytes (64KB)") + + # NETWORK: 65535 bytes max (2-byte length) + network = get_profile(Profile.NETWORK) + max_payload = 65535 if network.payload.length_bytes == 2 else 255 + assert max_payload == 65535 + print(f" ✓ NETWORK: max {max_payload} bytes (64KB)") + + print(" PASS: All profiles have correct payload size limits") + + +def test_profile_to_dict(): + """Test conversion to dictionary representation""" + print("\n[TEST] Profile Dictionary Representation") + + for profile in Profile: + definition = get_profile(profile) + data = definition.to_dict() + + # Verify required fields + assert 'name' in data + assert 'header_type' in data + assert 'payload_type' in data + assert 'total_overhead' in data + assert 'has_crc' in data + + print(f" ✓ {profile.name}: {data['name']} ({data['total_overhead']} bytes overhead)") + + print(" PASS: All profiles convert to dict correctly") + + +def main(): + """Run all profile tests""" + print("=" * 70) + print("Profile Test Suite") + print("=" * 70) + + try: + test_profile_definitions() + test_profile_features() + test_profile_payload_sizes() + test_profile_to_dict() + + print("\n" + "=" * 70) + print("✓ ALL PROFILE TESTS PASSED") + print("=" * 70) + return 0 + + except AssertionError as e: + print(f"\n✗ TEST FAILED: {e}") + import traceback + traceback.print_exc() + return 1 + except Exception as e: + print(f"\n✗ ERROR: {e}") + import traceback + traceback.print_exc() + return 1 + + +if __name__ == '__main__': + sys.exit(main())