Files
StreamLens/analyzer/protocols/decoders/base.py
2025-07-28 08:14:15 -04:00

162 lines
5.9 KiB
Python

"""
Base classes for Chapter 10 data type decoders
"""
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional, List, Union
from dataclasses import dataclass
import struct
@dataclass
class DecodedPayload:
"""Container for decoded Chapter 10 payload data"""
data_type: int
data_type_name: str
format_version: int
decoded_data: Dict[str, Any]
raw_payload: bytes
errors: List[str]
metadata: Dict[str, Any]
def __post_init__(self):
if self.errors is None:
self.errors = []
if self.metadata is None:
self.metadata = {}
class DataTypeDecoder(ABC):
"""Abstract base class for Chapter 10 data type decoders"""
def __init__(self):
self.supported_formats: List[int] = []
self.data_type_base: int = 0x00
self.data_type_name: str = "Unknown"
@abstractmethod
def can_decode(self, data_type: int) -> bool:
"""Check if this decoder can handle the given data type"""
pass
@abstractmethod
def decode(self, payload: bytes, ch10_header: Dict[str, Any]) -> Optional[DecodedPayload]:
"""Decode the payload data"""
pass
def get_data_type_name(self, data_type: int) -> str:
"""Get human-readable name for data type"""
return f"{self.data_type_name} Format {data_type & 0x0F}"
def _parse_intra_packet_header(self, payload: bytes, offset: int = 0) -> Optional[Dict[str, Any]]:
"""Parse common intra-packet header (IPH)"""
if len(payload) < offset + 8:
return None
try:
# Standard IPH format
iph_time = struct.unpack('<I', payload[offset:offset+4])[0]
reserved = struct.unpack('<H', payload[offset+4:offset+6])[0]
iph_length = struct.unpack('<H', payload[offset+6:offset+8])[0]
return {
'iph_time': iph_time,
'reserved': reserved,
'iph_length': iph_length,
'data_start': offset + 8
}
except struct.error:
return None
def _safe_unpack(self, format_str: str, data: bytes, offset: int = 0) -> Optional[tuple]:
"""Safely unpack binary data with error handling"""
try:
size = struct.calcsize(format_str)
if len(data) < offset + size:
return None
return struct.unpack(format_str, data[offset:offset+size])
except struct.error:
return None
class ContainerDecoder(DataTypeDecoder):
"""Decoder for containerized data formats"""
def decode_container(self, payload: bytes, ch10_header: Dict[str, Any]) -> List[DecodedPayload]:
"""Decode multiple embedded packets from container"""
decoded_packets = []
offset = 0
while offset < len(payload):
# Look for embedded CH-10 sync pattern
sync_offset = self._find_sync_pattern(payload, offset)
if sync_offset is None:
break
# Parse embedded header
embedded_header = self._parse_embedded_header(payload, sync_offset)
if not embedded_header:
break
# Extract embedded payload
embedded_payload = self._extract_embedded_payload(payload, sync_offset, embedded_header)
if embedded_payload:
# Recursively decode embedded packet
decoded = self.decode(embedded_payload, embedded_header)
if decoded:
decoded_packets.append(decoded)
# Move to next packet
offset = sync_offset + embedded_header.get('packet_length', 24)
return decoded_packets
def _find_sync_pattern(self, data: bytes, start_offset: int = 0) -> Optional[int]:
"""Find CH-10 sync pattern in data"""
sync_pattern = 0xEB25
for offset in range(start_offset, len(data) - 1):
if offset + 2 <= len(data):
word = struct.unpack('<H', data[offset:offset+2])[0]
if word == sync_pattern:
return offset
return None
def _parse_embedded_header(self, data: bytes, offset: int) -> Optional[Dict[str, Any]]:
"""Parse embedded CH-10 header"""
if len(data) < offset + 24:
return None
try:
sync_pattern = struct.unpack('<H', data[offset:offset+2])[0]
channel_id = struct.unpack('<H', data[offset+2:offset+4])[0]
packet_length = struct.unpack('<I', data[offset+4:offset+8])[0]
data_length = struct.unpack('<I', data[offset+8:offset+12])[0]
data_type = struct.unpack('<H', data[offset+12:offset+14])[0]
flags = struct.unpack('<H', data[offset+14:offset+16])[0]
time_bytes = data[offset+16:offset+22]
time_counter = int.from_bytes(time_bytes, 'little')
sequence = struct.unpack('<H', data[offset+22:offset+24])[0]
return {
'sync_pattern': sync_pattern,
'channel_id': channel_id,
'packet_length': packet_length,
'data_length': data_length,
'data_type': data_type,
'packet_flags': flags,
'relative_time_counter': time_counter,
'sequence_number': sequence
}
except struct.error:
return None
def _extract_embedded_payload(self, data: bytes, header_offset: int,
header: Dict[str, Any]) -> Optional[bytes]:
"""Extract payload from embedded packet"""
payload_start = header_offset + 24
payload_length = header.get('data_length', 0)
if payload_start + payload_length > len(data):
return None
return data[payload_start:payload_start + payload_length]