pretty good

This commit is contained in:
2025-07-28 08:14:15 -04:00
parent 36a576dc2c
commit 4dd632012f
21 changed files with 2174 additions and 152 deletions

View File

@@ -22,6 +22,7 @@ except ImportError:
sys.exit(1)
from .base import ProtocolDissector, DissectionResult, ProtocolType
from .decoders.registry import decoder_registry
class Chapter10Dissector(ProtocolDissector):
@@ -117,7 +118,18 @@ class Chapter10Dissector(ProtocolDissector):
if packet_length > 24 and payload_start + (packet_length - 24) <= len(raw_data):
result.payload = raw_data[payload_start:payload_start + (packet_length - 24)]
# Try to parse specific data formats
# Use new decoder framework for payload parsing
decoded_payload = decoder_registry.decode_payload(result.payload, header)
if decoded_payload:
result.fields['decoded_payload'] = {
'data_type_name': decoded_payload.data_type_name,
'format_version': decoded_payload.format_version,
'decoded_data': decoded_payload.decoded_data,
'decoder_errors': decoded_payload.errors,
'decoder_metadata': decoded_payload.metadata
}
# Legacy Ethernet Format 0 parsing (for backwards compatibility)
data_type = header.get('data_type', 0)
if data_type == 0x40: # Ethernet Format 0
eth_data = self._parse_ethernet_fmt0(result.payload)

View File

@@ -0,0 +1,34 @@
"""
Chapter 10 Data Type Decoders
Modular decoder framework for IRIG-106 Chapter 10 data types
"""
from .base import DataTypeDecoder, DecodedPayload
from .registry import DecoderRegistry
from .tspi_cts import TSPICTSDecoder, ACTTSDecoder, GPSNMEADecoder, EAGACMIDecoder
from .image import ImageDecoder
from .uart import UARTDecoder
from .ieee1394 import IEEE1394Decoder
from .parallel import ParallelDecoder
from .ethernet import EthernetDecoder
from .can_bus import CANBusDecoder
from .fibre_channel import FibreChannelDecoder
from .custom_timing import CustomTimingDecoder
__all__ = [
'DataTypeDecoder',
'DecodedPayload',
'DecoderRegistry',
'TSPICTSDecoder',
'ACTTSDecoder',
'GPSNMEADecoder',
'EAGACMIDecoder',
'ImageDecoder',
'UARTDecoder',
'IEEE1394Decoder',
'ParallelDecoder',
'EthernetDecoder',
'CANBusDecoder',
'FibreChannelDecoder',
'CustomTimingDecoder'
]

View File

@@ -0,0 +1,162 @@
"""
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]

View File

@@ -0,0 +1,105 @@
"""
CAN Bus decoder for Chapter 10 data types
Supports Controller Area Network Bus (0x78)
"""
import struct
from typing import Dict, Any, Optional
from .base import DataTypeDecoder, DecodedPayload
class CANBusDecoder(DataTypeDecoder):
"""Decoder for CAN Bus type (0x78)"""
def __init__(self):
super().__init__()
self.data_type_base = 0x78
self.data_type_name = "CAN Bus"
self.supported_formats = [0x78]
def can_decode(self, data_type: int) -> bool:
return data_type == 0x78
def get_data_type_name(self, data_type: int) -> str:
return "Controller Area Network Bus"
def decode(self, payload: bytes, ch10_header: Dict[str, Any]) -> Optional[DecodedPayload]:
"""Decode CAN Bus payload"""
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# Parse CAN messages
messages = []
offset = data_start
while offset + 16 <= len(payload):
can_header = self._safe_unpack('<IIII', payload, offset)
if not can_header:
break
can_timestamp, can_id_flags, data_length, reserved = can_header
# Parse CAN ID and flags
can_id = can_id_flags & 0x1FFFFFFF
extended_id = bool(can_id_flags & 0x80000000)
remote_frame = bool(can_id_flags & 0x40000000)
error_frame = bool(can_id_flags & 0x20000000)
message = {
'timestamp': can_timestamp,
'can_id': f'0x{can_id:x}',
'extended_id': extended_id,
'remote_frame': remote_frame,
'error_frame': error_frame,
'data_length': data_length
}
# Extract CAN data
can_data_start = offset + 16
if can_data_start + data_length <= len(payload):
can_data = payload[can_data_start:can_data_start + data_length]
message['data'] = can_data.hex()
message['data_bytes'] = list(can_data)
else:
message['data_error'] = 'Data extends beyond payload'
messages.append(message)
offset = can_data_start + max(data_length, 8) # CAN frames are padded to 8 bytes
if len(messages) >= 100: # Limit output size
break
decoded_data['can_messages'] = messages
decoded_data['message_count'] = len(messages)
# Statistics
if messages:
extended_count = sum(1 for msg in messages if msg['extended_id'])
remote_count = sum(1 for msg in messages if msg['remote_frame'])
error_count = sum(1 for msg in messages if msg['error_frame'])
decoded_data['statistics'] = {
'extended_frames': extended_count,
'remote_frames': remote_count,
'error_frames': error_count,
'standard_frames': len(messages) - extended_count
}
return DecodedPayload(
data_type=0x78,
data_type_name="Controller Area Network Bus",
format_version=0,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'CANBusDecoder'}
)

View File

@@ -0,0 +1,279 @@
"""
Custom timing decoder for proprietary Chapter 10 timing frames
Handles the 0x72xx-0x78xx timing sequence found in ACTTS-like systems
"""
import struct
from typing import Dict, Any, Optional, List
from .base import DataTypeDecoder, DecodedPayload
class CustomTimingDecoder(DataTypeDecoder):
"""Decoder for custom timing frames (0x7200-0x7899)"""
def __init__(self):
super().__init__()
self.data_type_base = 0x72
self.data_type_name = "Custom Timing"
self.supported_formats = []
# Support all 0x72xx - 0x78xx variants
for base in range(0x72, 0x79):
for variant in range(0x00, 0x100):
self.supported_formats.append((base << 8) | variant)
def can_decode(self, data_type: int) -> bool:
# Check if data type is in the 0x72xx-0x78xx range
return 0x7200 <= data_type <= 0x78FF
def get_data_type_name(self, data_type: int) -> str:
base = (data_type >> 8) & 0xFF
variant = data_type & 0xFF
timing_types = {
0x72: "Custom ACTTS Timing",
0x73: "Extended Timing Format",
0x74: "Sync Timing Format",
0x75: "Clock Reference Format",
0x76: "Time Correlation Format",
0x77: "Timing Validation Format",
0x78: "Multi-Source Timing"
}
base_name = timing_types.get(base, f"Timing Format 0x{base:02x}")
return f"{base_name} (Variant 0x{variant:02x})"
def decode(self, payload: bytes, ch10_header: Dict[str, Any]) -> Optional[DecodedPayload]:
"""Decode custom timing payload"""
data_type = ch10_header.get('data_type', 0)
if not self.can_decode(data_type):
return None
decoded_data = {}
errors = []
# Parse IPH if present
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
# For custom timing, missing IPH might be normal
# Analyze timing data structure
timing_analysis = self._analyze_timing_structure(payload, data_start, data_type)
decoded_data.update(timing_analysis)
# Extract CH-10 timing information from header
ch10_time = ch10_header.get('relative_time_counter', 0)
decoded_data['ch10_time_counter'] = ch10_time
decoded_data['ch10_sequence'] = ch10_header.get('sequence_number', 0)
decoded_data['ch10_channel'] = ch10_header.get('channel_id', 0)
# Calculate timing metrics
if 'timing_samples' in decoded_data and decoded_data['timing_samples']:
timing_metrics = self._calculate_timing_metrics(decoded_data['timing_samples'])
decoded_data['timing_metrics'] = timing_metrics
return DecodedPayload(
data_type=data_type,
data_type_name=self.get_data_type_name(data_type),
format_version=(data_type >> 8) & 0x0F,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'CustomTimingDecoder', 'timing_type': 'proprietary'}
)
def _analyze_timing_structure(self, payload: bytes, data_start: int, data_type: int) -> Dict[str, Any]:
"""Analyze the structure of timing data"""
analysis = {}
if data_start >= len(payload):
return {'error': 'No timing data available'}
timing_data = payload[data_start:]
analysis['timing_data_length'] = len(timing_data)
# Look for timing patterns
timing_samples = []
timestamps = []
# Try different word sizes for timing data
for word_size in [4, 8]:
if len(timing_data) >= word_size:
samples = self._extract_timing_words(timing_data, word_size)
if samples:
timing_samples.extend(samples[:50]) # Limit to first 50 samples
analysis['timing_samples'] = timing_samples
analysis['sample_count'] = len(timing_samples)
# Look for embedded timing markers
timing_markers = self._find_timing_markers(timing_data)
if timing_markers:
analysis['timing_markers'] = timing_markers
# Detect timing format based on data type
base_type = (data_type >> 8) & 0xFF
if base_type == 0x72:
# ACTTS-style timing
actts_analysis = self._analyze_actts_timing(timing_data)
analysis.update(actts_analysis)
elif base_type in [0x73, 0x74, 0x75, 0x76, 0x77]:
# Extended timing formats
extended_analysis = self._analyze_extended_timing(timing_data, base_type)
analysis.update(extended_analysis)
elif base_type == 0x78:
# Multi-source timing
multi_analysis = self._analyze_multi_source_timing(timing_data)
analysis.update(multi_analysis)
return analysis
def _extract_timing_words(self, data: bytes, word_size: int) -> List[int]:
"""Extract timing words from binary data"""
words = []
format_str = '<I' if word_size == 4 else '<Q'
for i in range(0, len(data) - word_size + 1, word_size):
try:
word = struct.unpack(format_str, data[i:i+word_size])[0]
words.append(word)
except struct.error:
break
if len(words) >= 100: # Limit extraction
break
return words
def _find_timing_markers(self, data: bytes) -> List[Dict[str, Any]]:
"""Find timing synchronization markers in data"""
markers = []
# Common timing sync patterns
sync_patterns = [
b'\x81\x81\x81\x81', # Potential sync pattern
b'\x82\x82\x82\x82', # Another potential pattern
b'\xa9\xa9\xa9\xa9', # Observed in your data
]
for pattern in sync_patterns:
offset = 0
while True:
pos = data.find(pattern, offset)
if pos == -1:
break
markers.append({
'pattern': pattern.hex(),
'offset': pos,
'description': f'Sync pattern at offset {pos}'
})
offset = pos + 1
if len(markers) >= 20: # Limit markers
break
return markers
def _analyze_actts_timing(self, data: bytes) -> Dict[str, Any]:
"""Analyze ACTTS-style timing data"""
analysis = {'timing_format': 'ACTTS-style'}
if len(data) >= 16:
# Look for ACTTS-like header
try:
header = struct.unpack('<IIII', data[0:16])
analysis['actts_header'] = {
'word1': f'0x{header[0]:08x}',
'word2': f'0x{header[1]:08x}',
'word3': f'0x{header[2]:08x}',
'word4': f'0x{header[3]:08x}'
}
# Check for timing correlation
if header[1] - header[0] in [1, 1000, 1000000]:
analysis['timing_correlation'] = 'Incremental timing detected'
except struct.error:
analysis['actts_parse_error'] = 'Failed to parse ACTTS header'
return analysis
def _analyze_extended_timing(self, data: bytes, base_type: int) -> Dict[str, Any]:
"""Analyze extended timing formats (0x73-0x77)"""
analysis = {'timing_format': f'Extended Format 0x{base_type:02x}'}
# Look for timing sequences
if len(data) >= 8:
try:
seq_data = struct.unpack('<HH', data[0:4])
analysis['sequence_info'] = {
'seq1': seq_data[0],
'seq2': seq_data[1],
'delta': seq_data[1] - seq_data[0]
}
except struct.error:
pass
return analysis
def _analyze_multi_source_timing(self, data: bytes) -> Dict[str, Any]:
"""Analyze multi-source timing data (0x78)"""
analysis = {'timing_format': 'Multi-source timing'}
# Look for multiple timing sources
sources = []
offset = 0
while offset + 8 <= len(data):
try:
source_data = struct.unpack('<II', data[offset:offset+8])
sources.append({
'source_id': source_data[0] & 0xFF,
'timestamp': source_data[1],
'offset': offset
})
offset += 8
except struct.error:
break
if len(sources) >= 10: # Limit sources
break
analysis['timing_sources'] = sources
analysis['source_count'] = len(sources)
return analysis
def _calculate_timing_metrics(self, samples: List[int]) -> Dict[str, Any]:
"""Calculate timing statistics from samples"""
if not samples or len(samples) < 2:
return {}
# Calculate deltas
deltas = [samples[i+1] - samples[i] for i in range(len(samples)-1)]
# Basic statistics
metrics = {
'sample_count': len(samples),
'min_value': min(samples),
'max_value': max(samples),
'range': max(samples) - min(samples),
'first_sample': samples[0],
'last_sample': samples[-1]
}
if deltas:
metrics.update({
'min_delta': min(deltas),
'max_delta': max(deltas),
'avg_delta': sum(deltas) / len(deltas),
'zero_deltas': deltas.count(0),
'constant_rate': len(set(deltas)) == 1
})
return metrics

View File

@@ -0,0 +1,316 @@
"""
Ethernet Data decoders for Chapter 10 data types
Supports Ethernet Data Formats 0-1 (0x68-0x69)
"""
import struct
from typing import Dict, Any, Optional
from .base import DataTypeDecoder, DecodedPayload
class EthernetDecoder(DataTypeDecoder):
"""Decoder for Ethernet Data types (0x68-0x69)"""
def __init__(self):
super().__init__()
self.data_type_base = 0x68
self.data_type_name = "Ethernet Data"
self.supported_formats = [0x68, 0x69]
def can_decode(self, data_type: int) -> bool:
return data_type in [0x68, 0x69]
def get_data_type_name(self, data_type: int) -> str:
format_names = {
0x68: "Ethernet Data Format 0",
0x69: "Ethernet UDP Payload"
}
return format_names.get(data_type, f"Ethernet Format {data_type & 0x0F}")
def decode(self, payload: bytes, ch10_header: Dict[str, Any]) -> Optional[DecodedPayload]:
"""Decode Ethernet payload"""
data_type = ch10_header.get('data_type', 0)
if not self.can_decode(data_type):
return None
if data_type == 0x68:
return self._decode_ethernet_format0(payload, ch10_header)
elif data_type == 0x69:
return self._decode_ethernet_udp_payload(payload, ch10_header)
return None
def _decode_ethernet_format0(self, payload: bytes, ch10_header: Dict[str, Any]) -> DecodedPayload:
"""Decode Ethernet Format 0 (Full Ethernet Frame)"""
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# Parse Ethernet Format 0 header
if data_start + 12 <= len(payload):
eth_header = self._safe_unpack('<III', payload, data_start)
if eth_header:
frame_status = eth_header[2]
decoded_data.update({
'ethernet_timestamp': eth_header[1],
'frame_status_word': frame_status,
'frame_length': frame_status & 0x3FFF,
'length_error': bool(frame_status & 0x8000),
'crc_error': bool(frame_status & 0x10000),
'content_type': (frame_status >> 28) & 0x3
})
# Decode content type
content_types = {
0: "Full MAC frame",
1: "Payload only",
2: "Reserved",
3: "Reserved"
}
decoded_data['content_type_description'] = content_types.get(
decoded_data['content_type'], "Unknown"
)
# Extract Ethernet frame data
frame_data_start = data_start + 12
frame_length = decoded_data['frame_length']
if frame_data_start + frame_length <= len(payload):
frame_data = payload[frame_data_start:frame_data_start + frame_length]
# Parse Ethernet header if full MAC frame
if decoded_data['content_type'] == 0 and len(frame_data) >= 14:
eth_parsed = self._parse_ethernet_header(frame_data)
decoded_data.update(eth_parsed)
else:
decoded_data['raw_frame_data'] = frame_data[:64].hex() # First 64 bytes
decoded_data['actual_frame_length'] = len(frame_data)
else:
errors.append("Frame data extends beyond payload")
else:
errors.append("Failed to parse Ethernet Format 0 header")
else:
errors.append("Insufficient data for Ethernet header")
return DecodedPayload(
data_type=0x68,
data_type_name="Ethernet Data Format 0",
format_version=0,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'EthernetDecoder'}
)
def _decode_ethernet_udp_payload(self, payload: bytes, ch10_header: Dict[str, Any]) -> DecodedPayload:
"""Decode Ethernet UDP Payload (Format 1)"""
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# Parse UDP payload header
if data_start + 16 <= len(payload):
udp_header = self._safe_unpack('<IIHHHH', payload, data_start)
if udp_header:
decoded_data.update({
'udp_timestamp': udp_header[1],
'src_ip': self._ip_to_string(udp_header[2]),
'dst_ip': self._ip_to_string(udp_header[3]),
'src_port': udp_header[4],
'dst_port': udp_header[5]
})
# Extract UDP payload
udp_payload_start = data_start + 16
if udp_payload_start < len(payload):
udp_payload = payload[udp_payload_start:]
decoded_data['udp_payload_length'] = len(udp_payload)
decoded_data['udp_payload_preview'] = udp_payload[:64].hex()
# Try to identify payload content
payload_info = self._analyze_udp_payload(udp_payload)
decoded_data.update(payload_info)
else:
errors.append("Failed to parse UDP header")
else:
errors.append("Insufficient data for UDP header")
return DecodedPayload(
data_type=0x69,
data_type_name="Ethernet UDP Payload",
format_version=1,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'EthernetDecoder'}
)
def _parse_ethernet_header(self, frame_data: bytes) -> Dict[str, Any]:
"""Parse Ethernet MAC header"""
if len(frame_data) < 14:
return {'eth_parse_error': 'Insufficient data for Ethernet header'}
# Parse MAC addresses and EtherType
dst_mac = frame_data[0:6]
src_mac = frame_data[6:12]
ethertype = struct.unpack('>H', frame_data[12:14])[0]
eth_data = {
'dst_mac': ':'.join(f'{b:02x}' for b in dst_mac),
'src_mac': ':'.join(f'{b:02x}' for b in src_mac),
'ethertype': f'0x{ethertype:04x}',
'ethertype_description': self._decode_ethertype(ethertype)
}
# Parse payload based on EtherType
if ethertype == 0x0800 and len(frame_data) >= 34: # IPv4
ip_data = self._parse_ip_header(frame_data[14:])
eth_data.update(ip_data)
elif ethertype == 0x0806 and len(frame_data) >= 42: # ARP
arp_data = self._parse_arp_header(frame_data[14:])
eth_data.update(arp_data)
return eth_data
def _parse_ip_header(self, ip_data: bytes) -> Dict[str, Any]:
"""Parse IPv4 header"""
if len(ip_data) < 20:
return {'ip_parse_error': 'Insufficient data for IP header'}
version_ihl = ip_data[0]
version = (version_ihl >> 4) & 0x0F
ihl = version_ihl & 0x0F
if version != 4:
return {'ip_parse_error': f'Unsupported IP version: {version}'}
tos, total_length, identification, flags_fragment = struct.unpack('>BHHH', ip_data[1:9])
ttl, protocol, checksum = struct.unpack('>BBH', ip_data[8:12])
src_ip = struct.unpack('>I', ip_data[12:16])[0]
dst_ip = struct.unpack('>I', ip_data[16:20])[0]
return {
'ip_version': version,
'ip_header_length': ihl * 4,
'ip_tos': tos,
'ip_total_length': total_length,
'ip_id': identification,
'ip_ttl': ttl,
'ip_protocol': protocol,
'ip_src': self._ip_to_string(src_ip),
'ip_dst': self._ip_to_string(dst_ip),
'ip_protocol_name': self._decode_ip_protocol(protocol)
}
def _parse_arp_header(self, arp_data: bytes) -> Dict[str, Any]:
"""Parse ARP header"""
if len(arp_data) < 28:
return {'arp_parse_error': 'Insufficient data for ARP header'}
hw_type, proto_type, hw_len, proto_len, opcode = struct.unpack('>HHBBH', arp_data[0:8])
sender_hw = arp_data[8:14]
sender_proto = struct.unpack('>I', arp_data[14:18])[0]
target_hw = arp_data[18:24]
target_proto = struct.unpack('>I', arp_data[24:28])[0]
return {
'arp_hw_type': hw_type,
'arp_proto_type': f'0x{proto_type:04x}',
'arp_opcode': opcode,
'arp_opcode_description': 'Request' if opcode == 1 else 'Reply' if opcode == 2 else f'Unknown ({opcode})',
'arp_sender_hw': ':'.join(f'{b:02x}' for b in sender_hw),
'arp_sender_ip': self._ip_to_string(sender_proto),
'arp_target_hw': ':'.join(f'{b:02x}' for b in target_hw),
'arp_target_ip': self._ip_to_string(target_proto)
}
def _analyze_udp_payload(self, payload: bytes) -> Dict[str, Any]:
"""Analyze UDP payload content"""
analysis = {}
if len(payload) == 0:
return {'payload_analysis': 'Empty payload'}
# Check for common protocols
if len(payload) >= 4:
# Check for DNS (port 53 patterns)
if payload[0:2] in [b'\x00\x01', b'\x81\x80', b'\x01\x00']:
analysis['possible_protocol'] = 'DNS'
# Check for DHCP magic cookie
elif payload[:4] == b'\x63\x82\x53\x63':
analysis['possible_protocol'] = 'DHCP'
# Check for RTP (version 2)
elif (payload[0] & 0xC0) == 0x80:
analysis['possible_protocol'] = 'RTP'
else:
analysis['possible_protocol'] = 'Unknown'
# Basic statistics
analysis['payload_entropy'] = self._calculate_entropy(payload[:256]) # First 256 bytes
analysis['null_bytes'] = payload.count(0)
analysis['printable_chars'] = sum(1 for b in payload[:256] if 32 <= b <= 126)
return analysis
def _calculate_entropy(self, data: bytes) -> float:
"""Calculate Shannon entropy of data"""
if not data:
return 0.0
counts = [0] * 256
for byte in data:
counts[byte] += 1
entropy = 0.0
length = len(data)
for count in counts:
if count > 0:
p = count / length
entropy -= p * (p.bit_length() - 1) # log2(p)
return entropy
def _ip_to_string(self, ip_int: int) -> str:
"""Convert 32-bit integer to IP address string"""
return f"{(ip_int >> 24) & 0xFF}.{(ip_int >> 16) & 0xFF}.{(ip_int >> 8) & 0xFF}.{ip_int & 0xFF}"
def _decode_ethertype(self, ethertype: int) -> str:
"""Decode EtherType field"""
types = {
0x0800: "IPv4",
0x0806: "ARP",
0x86DD: "IPv6",
0x8100: "VLAN",
0x88F7: "PTP"
}
return types.get(ethertype, f"Unknown (0x{ethertype:04x})")
def _decode_ip_protocol(self, protocol: int) -> str:
"""Decode IP protocol field"""
protocols = {
1: "ICMP",
6: "TCP",
17: "UDP",
89: "OSPF",
132: "SCTP"
}
return protocols.get(protocol, f"Unknown ({protocol})")

View File

@@ -0,0 +1,166 @@
"""
Fibre Channel Data decoder for Chapter 10 data types
Supports Fibre Channel Format 0 (0x79)
"""
import struct
from typing import Dict, Any, Optional
from .base import DataTypeDecoder, DecodedPayload
class FibreChannelDecoder(DataTypeDecoder):
"""Decoder for Fibre Channel Data type (0x79)"""
def __init__(self):
super().__init__()
self.data_type_base = 0x79
self.data_type_name = "Fibre Channel Data"
self.supported_formats = [0x79]
def can_decode(self, data_type: int) -> bool:
return data_type == 0x79
def get_data_type_name(self, data_type: int) -> str:
return "Fibre Channel Data Format 0"
def decode(self, payload: bytes, ch10_header: Dict[str, Any]) -> Optional[DecodedPayload]:
"""Decode Fibre Channel payload"""
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# Parse FC frame header
if data_start + 24 <= len(payload):
# FC frame header (simplified)
fc_header = self._safe_unpack('<IIIIII', payload, data_start)
if fc_header:
decoded_data.update({
'fc_timestamp': fc_header[0],
'fc_frame_length': fc_header[1],
'fc_r_ctl': (fc_header[2] >> 24) & 0xFF,
'fc_d_id': fc_header[2] & 0xFFFFFF,
'fc_cs_ctl': (fc_header[3] >> 24) & 0xFF,
'fc_s_id': fc_header[3] & 0xFFFFFF,
'fc_type': (fc_header[4] >> 24) & 0xFF,
'fc_f_ctl': fc_header[4] & 0xFFFFFF,
'fc_seq_id': (fc_header[5] >> 24) & 0xFF,
'fc_df_ctl': (fc_header[5] >> 16) & 0xFF,
'fc_seq_cnt': fc_header[5] & 0xFFFF
})
# Decode R_CTL field
r_ctl = decoded_data['fc_r_ctl']
decoded_data['fc_r_ctl_description'] = self._decode_r_ctl(r_ctl)
# Decode Type field
fc_type = decoded_data['fc_type']
decoded_data['fc_type_description'] = self._decode_fc_type(fc_type)
# Extract payload
fc_payload_start = data_start + 24
frame_length = decoded_data['fc_frame_length']
if fc_payload_start + frame_length <= len(payload):
fc_payload = payload[fc_payload_start:fc_payload_start + frame_length]
decoded_data['fc_payload_length'] = len(fc_payload)
decoded_data['fc_payload_preview'] = fc_payload[:64].hex()
# Analyze payload based on type
if fc_type == 0x08: # SCSI FCP
scsi_data = self._parse_scsi_fcp(fc_payload)
decoded_data.update(scsi_data)
else:
errors.append("FC payload extends beyond packet")
else:
errors.append("Failed to parse FC header")
return DecodedPayload(
data_type=0x79,
data_type_name="Fibre Channel Data Format 0",
format_version=0,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'FibreChannelDecoder'}
)
def _decode_r_ctl(self, r_ctl: int) -> str:
"""Decode R_CTL field"""
r_ctl_types = {
0x00: "Device Data",
0x01: "Extended Link Data",
0x02: "FC-4 Link Data",
0x03: "Video Data",
0x20: "Basic Link Data",
0x21: "ACK_1",
0x22: "ACK_0",
0x23: "P_RJT",
0x24: "F_RJT",
0x25: "P_BSY",
0x26: "F_BSY"
}
return r_ctl_types.get(r_ctl, f"Unknown (0x{r_ctl:02x})")
def _decode_fc_type(self, fc_type: int) -> str:
"""Decode FC Type field"""
fc_types = {
0x00: "Basic Link Service",
0x01: "Extended Link Service",
0x04: "IP over FC",
0x05: "ATM over FC",
0x08: "SCSI FCP",
0x09: "SCSI GPP",
0x0A: "IPI-3 Master",
0x0B: "IPI-3 Slave",
0x0C: "IPI-3 Peer"
}
return fc_types.get(fc_type, f"Unknown (0x{fc_type:02x})")
def _parse_scsi_fcp(self, payload: bytes) -> Dict[str, Any]:
"""Parse SCSI FCP payload"""
scsi_data = {}
if len(payload) >= 32:
# FCP_CMND structure
lun = payload[0:8]
task_codes = payload[8]
task_mgmt = payload[9]
add_cdb_len = payload[10]
rddata = bool(payload[11] & 0x02)
wrdata = bool(payload[11] & 0x01)
scsi_data.update({
'scsi_lun': lun.hex(),
'scsi_task_codes': task_codes,
'scsi_task_mgmt': task_mgmt,
'scsi_rddata': rddata,
'scsi_wrdata': wrdata
})
# CDB starts at offset 12
if len(payload) >= 16:
cdb = payload[12:16]
scsi_data['scsi_cdb'] = cdb.hex()
if cdb[0] in [0x12, 0x00, 0x28, 0x2A]: # Common SCSI commands
scsi_data['scsi_command'] = self._decode_scsi_command(cdb[0])
return scsi_data
def _decode_scsi_command(self, opcode: int) -> str:
"""Decode SCSI command opcode"""
commands = {
0x00: "TEST UNIT READY",
0x12: "INQUIRY",
0x28: "READ(10)",
0x2A: "WRITE(10)",
0x35: "SYNCHRONIZE CACHE",
0x3C: "READ BUFFER"
}
return commands.get(opcode, f"Unknown (0x{opcode:02x})")

View File

@@ -0,0 +1,142 @@
"""
IEEE 1394 Data decoders for Chapter 10 data types
Supports IEEE 1394 Formats 0-1 (0x58-0x59)
"""
import struct
from typing import Dict, Any, Optional
from .base import DataTypeDecoder, DecodedPayload
class IEEE1394Decoder(DataTypeDecoder):
"""Decoder for IEEE 1394 Data types (0x58-0x59)"""
def __init__(self):
super().__init__()
self.data_type_base = 0x58
self.data_type_name = "IEEE 1394 Data"
self.supported_formats = [0x58, 0x59]
def can_decode(self, data_type: int) -> bool:
return data_type in [0x58, 0x59]
def get_data_type_name(self, data_type: int) -> str:
format_names = {
0x58: "IEEE 1394 Transaction",
0x59: "IEEE 1394 Physical Layer"
}
return format_names.get(data_type, f"IEEE 1394 Format {data_type & 0x0F}")
def decode(self, payload: bytes, ch10_header: Dict[str, Any]) -> Optional[DecodedPayload]:
"""Decode IEEE 1394 payload"""
data_type = ch10_header.get('data_type', 0)
if not self.can_decode(data_type):
return None
if data_type == 0x58:
return self._decode_transaction(payload, ch10_header)
elif data_type == 0x59:
return self._decode_physical_layer(payload, ch10_header)
return None
def _decode_transaction(self, payload: bytes, ch10_header: Dict[str, Any]) -> DecodedPayload:
"""Decode IEEE 1394 Transaction data"""
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# Parse 1394 transaction header
if data_start + 16 <= len(payload):
tx_header = self._safe_unpack('<IIII', payload, data_start)
if tx_header:
decoded_data.update({
'transaction_timestamp': tx_header[0],
'transaction_code': tx_header[1] & 0x0F,
'source_id': (tx_header[1] >> 16) & 0xFFFF,
'destination_offset': tx_header[2],
'data_length': tx_header[3]
})
# Decode transaction code
tx_codes = {
0: "Write Request",
1: "Write Response",
4: "Read Request",
5: "Read Response",
6: "Lock Request",
7: "Lock Response"
}
decoded_data['transaction_type'] = tx_codes.get(
decoded_data['transaction_code'],
f"Unknown ({decoded_data['transaction_code']})"
)
# Extract transaction data
tx_data_start = data_start + 16
tx_data_length = decoded_data['data_length']
if tx_data_start + tx_data_length <= len(payload):
tx_data = payload[tx_data_start:tx_data_start + tx_data_length]
decoded_data['transaction_data'] = tx_data[:64].hex()
else:
errors.append("Failed to parse 1394 transaction header")
return DecodedPayload(
data_type=0x58,
data_type_name="IEEE 1394 Transaction",
format_version=0,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'IEEE1394Decoder'}
)
def _decode_physical_layer(self, payload: bytes, ch10_header: Dict[str, Any]) -> DecodedPayload:
"""Decode IEEE 1394 Physical Layer data"""
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# Parse physical layer data
if data_start < len(payload):
phy_data = payload[data_start:]
decoded_data['phy_data_length'] = len(phy_data)
decoded_data['phy_data_hex'] = phy_data[:64].hex()
# Basic PHY packet analysis
if len(phy_data) >= 4:
phy_header = struct.unpack('<I', phy_data[:4])[0]
decoded_data['phy_packet_type'] = (phy_header >> 28) & 0x0F
decoded_data['phy_speed'] = (phy_header >> 26) & 0x03
speed_names = {0: "100 Mbps", 1: "200 Mbps", 2: "400 Mbps", 3: "Reserved"}
decoded_data['phy_speed_description'] = speed_names.get(
decoded_data['phy_speed'], "Unknown"
)
return DecodedPayload(
data_type=0x59,
data_type_name="IEEE 1394 Physical Layer",
format_version=1,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'IEEE1394Decoder'}
)

View File

@@ -0,0 +1,186 @@
"""
Image Data decoders for Chapter 10 data types
Supports Image Data Formats 2-7 (0x4A-0x4F)
"""
import struct
from typing import Dict, Any, Optional, Tuple
from .base import DataTypeDecoder, DecodedPayload
class ImageDecoder(DataTypeDecoder):
"""Decoder for Image Data types (0x4A-0x4F)"""
def __init__(self):
super().__init__()
self.data_type_base = 0x4A
self.data_type_name = "Image Data"
self.supported_formats = list(range(0x4A, 0x50))
def can_decode(self, data_type: int) -> bool:
return 0x4A <= data_type <= 0x4F
def get_data_type_name(self, data_type: int) -> str:
format_names = {
0x4A: "Image Data Format 2 (Dynamic Imagery)",
0x4B: "Image Data Format 3",
0x4C: "Image Data Format 4",
0x4D: "Image Data Format 5",
0x4E: "Image Data Format 6",
0x4F: "Image Data Format 7"
}
return format_names.get(data_type, f"Image Data Format {data_type & 0x0F}")
def decode(self, payload: bytes, ch10_header: Dict[str, Any]) -> Optional[DecodedPayload]:
"""Decode Image Data payload"""
data_type = ch10_header.get('data_type', 0)
if not self.can_decode(data_type):
return None
if data_type == 0x4A:
return self._decode_dynamic_imagery(payload, ch10_header)
else:
return self._decode_generic_image(payload, ch10_header)
def _decode_dynamic_imagery(self, payload: bytes, ch10_header: Dict[str, Any]) -> DecodedPayload:
"""Decode Dynamic Imagery (Format 2)"""
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# Parse image header
if data_start + 32 <= len(payload):
img_header = self._safe_unpack('<IIHHHHHHHHHH', payload, data_start)
if img_header:
decoded_data.update({
'image_timestamp': img_header[0],
'image_id': img_header[1],
'image_format': img_header[2],
'image_width': img_header[3],
'image_height': img_header[4],
'bits_per_pixel': img_header[5],
'compression_type': img_header[6],
'image_size': img_header[7],
'x_offset': img_header[8],
'y_offset': img_header[9],
'frame_number': img_header[10],
'reserved': img_header[11]
})
# Decode format and compression
decoded_data['format_description'] = self._decode_image_format(img_header[2])
decoded_data['compression_description'] = self._decode_compression(img_header[6])
# Extract image data
image_data_start = data_start + 32
image_size = img_header[7]
if image_data_start + image_size <= len(payload):
image_data = payload[image_data_start:image_data_start + image_size]
decoded_data['image_data_length'] = len(image_data)
decoded_data['image_data_hash'] = hash(image_data) & 0xFFFFFFFF
# Don't include raw image data in output for performance
# Store first few bytes for analysis
decoded_data['image_header_bytes'] = image_data[:16].hex()
else:
errors.append("Image data extends beyond payload")
else:
errors.append("Failed to parse image header")
else:
errors.append("Insufficient data for image header")
return DecodedPayload(
data_type=0x4A,
data_type_name="Dynamic Imagery",
format_version=2,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'ImageDecoder'}
)
def _decode_generic_image(self, payload: bytes, ch10_header: Dict[str, Any]) -> DecodedPayload:
"""Decode generic image data (Formats 3-7)"""
data_type = ch10_header.get('data_type', 0)
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# Generic image data parsing
if data_start < len(payload):
image_data = payload[data_start:]
decoded_data['image_data_length'] = len(image_data)
decoded_data['image_data_hash'] = hash(image_data) & 0xFFFFFFFF
decoded_data['header_bytes'] = image_data[:32].hex() if len(image_data) >= 32 else image_data.hex()
# Try to identify image format from magic bytes
format_info = self._identify_image_format(image_data)
decoded_data.update(format_info)
return DecodedPayload(
data_type=data_type,
data_type_name=self.get_data_type_name(data_type),
format_version=data_type & 0x0F,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'ImageDecoder'}
)
def _decode_image_format(self, format_code: int) -> str:
"""Decode image format code"""
formats = {
0: "Monochrome",
1: "RGB",
2: "YUV 4:2:2",
3: "YUV 4:2:0",
4: "RGBA",
5: "Bayer Pattern"
}
return formats.get(format_code, f"Unknown ({format_code})")
def _decode_compression(self, compression_code: int) -> str:
"""Decode compression type"""
compressions = {
0: "Uncompressed",
1: "JPEG",
2: "H.264",
3: "MPEG-2",
4: "PNG",
5: "Lossless"
}
return compressions.get(compression_code, f"Unknown ({compression_code})")
def _identify_image_format(self, data: bytes) -> Dict[str, Any]:
"""Identify image format from magic bytes"""
if len(data) < 8:
return {'detected_format': 'Unknown (insufficient data)'}
# Check common image formats
if data[:2] == b'\xFF\xD8':
return {'detected_format': 'JPEG', 'magic_bytes': data[:4].hex()}
elif data[:8] == b'\x89PNG\r\n\x1a\n':
return {'detected_format': 'PNG', 'magic_bytes': data[:8].hex()}
elif data[:4] in [b'RIFF', b'AVI ']:
return {'detected_format': 'AVI/RIFF', 'magic_bytes': data[:4].hex()}
elif data[:4] == b'\x00\x00\x00\x20' or data[:4] == b'\x00\x00\x00\x18':
return {'detected_format': 'AVIF/HEIF', 'magic_bytes': data[:4].hex()}
else:
return {'detected_format': 'Unknown/Raw', 'magic_bytes': data[:8].hex()}

View File

@@ -0,0 +1,83 @@
"""
Parallel Data decoder for Chapter 10 data types
Supports Parallel Data Format 0 (0x60)
"""
import struct
from typing import Dict, Any, Optional
from .base import DataTypeDecoder, DecodedPayload
class ParallelDecoder(DataTypeDecoder):
"""Decoder for Parallel Data type (0x60)"""
def __init__(self):
super().__init__()
self.data_type_base = 0x60
self.data_type_name = "Parallel Data"
self.supported_formats = [0x60]
def can_decode(self, data_type: int) -> bool:
return data_type == 0x60
def get_data_type_name(self, data_type: int) -> str:
return "Parallel Data Format 0"
def decode(self, payload: bytes, ch10_header: Dict[str, Any]) -> Optional[DecodedPayload]:
"""Decode Parallel Data payload"""
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# Parse parallel data header
if data_start + 8 <= len(payload):
par_header = self._safe_unpack('<II', payload, data_start)
if par_header:
decoded_data.update({
'parallel_timestamp': par_header[0],
'parallel_status': par_header[1]
})
# Decode status bits
status = par_header[1]
decoded_data['data_valid'] = bool(status & 0x01)
decoded_data['clock_valid'] = bool(status & 0x02)
decoded_data['sync_detected'] = bool(status & 0x04)
decoded_data['error_detected'] = bool(status & 0x08)
# Extract parallel data
par_data_start = data_start + 8
if par_data_start < len(payload):
par_data = payload[par_data_start:]
decoded_data['parallel_data_length'] = len(par_data)
decoded_data['parallel_data_hex'] = par_data[:128].hex()
# Analyze data patterns
if len(par_data) >= 4:
# Sample first few words
words = []
for i in range(0, min(len(par_data), 32), 4):
if i + 4 <= len(par_data):
word = struct.unpack('<I', par_data[i:i+4])[0]
words.append(f'0x{word:08x}')
decoded_data['sample_words'] = words
else:
errors.append("Failed to parse parallel data header")
return DecodedPayload(
data_type=0x60,
data_type_name="Parallel Data Format 0",
format_version=0,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'ParallelDecoder'}
)

View File

@@ -0,0 +1,126 @@
"""
Decoder registry for Chapter 10 data types
"""
from typing import Dict, List, Optional, Type
from .base import DataTypeDecoder, DecodedPayload
class DecoderRegistry:
"""Registry for Chapter 10 data type decoders"""
def __init__(self):
self._decoders: Dict[int, DataTypeDecoder] = {}
self._decoder_classes: Dict[str, Type[DataTypeDecoder]] = {}
self._register_default_decoders()
def register_decoder(self, decoder: DataTypeDecoder, data_types: List[int]):
"""Register a decoder for specific data types"""
for data_type in data_types:
self._decoders[data_type] = decoder
def register_decoder_class(self, name: str, decoder_class: Type[DataTypeDecoder]):
"""Register a decoder class by name"""
self._decoder_classes[name] = decoder_class
def get_decoder(self, data_type: int) -> Optional[DataTypeDecoder]:
"""Get decoder for specific data type"""
return self._decoders.get(data_type)
def decode_payload(self, payload: bytes, ch10_header: Dict[str, any]) -> Optional[DecodedPayload]:
"""Decode payload using appropriate decoder"""
data_type = ch10_header.get('data_type', 0)
decoder = self.get_decoder(data_type)
if decoder:
return decoder.decode(payload, ch10_header)
# Return basic decoded payload if no specific decoder found
return DecodedPayload(
data_type=data_type,
data_type_name=f"Unknown (0x{data_type:02X})",
format_version=data_type & 0x0F,
decoded_data={'raw_data': payload.hex()},
raw_payload=payload,
errors=[f"No decoder available for data type 0x{data_type:02X}"],
metadata={'decoder': 'fallback'}
)
def list_supported_types(self) -> List[int]:
"""List all supported data types"""
return sorted(self._decoders.keys())
def get_decoder_info(self) -> Dict[int, str]:
"""Get information about registered decoders"""
return {
data_type: decoder.__class__.__name__
for data_type, decoder in self._decoders.items()
}
def _register_default_decoders(self):
"""Register default decoders for known data types"""
from .tspi_cts import TSPICTSDecoder, ACTTSDecoder, GPSNMEADecoder, EAGACMIDecoder
from .image import ImageDecoder
from .uart import UARTDecoder
from .ieee1394 import IEEE1394Decoder
from .parallel import ParallelDecoder
from .ethernet import EthernetDecoder
from .can_bus import CANBusDecoder
from .fibre_channel import FibreChannelDecoder
from .custom_timing import CustomTimingDecoder
# Register custom timing decoder for proprietary formats
custom_timing_decoder = CustomTimingDecoder()
# Register for all 0x72xx-0x78xx variants
custom_timing_types = []
for base in range(0x72, 0x79):
for variant in range(0x00, 0x100):
custom_timing_types.append((base << 8) | variant)
self.register_decoder(custom_timing_decoder, custom_timing_types)
# Register TSPI/CTS decoders
tspi_decoder = TSPICTSDecoder()
self.register_decoder(tspi_decoder, list(range(0x70, 0x78)))
# Register specific TSPI/CTS decoders for better handling
self.register_decoder(GPSNMEADecoder(), [0x70])
self.register_decoder(EAGACMIDecoder(), [0x71])
self.register_decoder(ACTTSDecoder(), [0x72])
# Register Image decoders
image_decoder = ImageDecoder()
self.register_decoder(image_decoder, list(range(0x4A, 0x50)))
# Register other decoders
self.register_decoder(UARTDecoder(), [0x50])
ieee1394_decoder = IEEE1394Decoder()
self.register_decoder(ieee1394_decoder, [0x58, 0x59])
self.register_decoder(ParallelDecoder(), [0x60])
ethernet_decoder = EthernetDecoder()
self.register_decoder(ethernet_decoder, [0x68, 0x69])
self.register_decoder(CANBusDecoder(), [0x78])
self.register_decoder(FibreChannelDecoder(), [0x79])
# Register decoder classes for factory pattern
self._decoder_classes.update({
'TSPICTSDecoder': TSPICTSDecoder,
'ACTTSDecoder': ACTTSDecoder,
'GPSNMEADecoder': GPSNMEADecoder,
'EAGACMIDecoder': EAGACMIDecoder,
'ImageDecoder': ImageDecoder,
'UARTDecoder': UARTDecoder,
'IEEE1394Decoder': IEEE1394Decoder,
'ParallelDecoder': ParallelDecoder,
'EthernetDecoder': EthernetDecoder,
'CANBusDecoder': CANBusDecoder,
'FibreChannelDecoder': FibreChannelDecoder,
'CustomTimingDecoder': CustomTimingDecoder
})
# Global decoder registry instance
decoder_registry = DecoderRegistry()

View File

@@ -0,0 +1,315 @@
"""
TSPI/CTS (Time Space Position Information/Common Test System) data decoders
Supports ACTTS, GPS NMEA-RTCM, and EAG ACMI formats
"""
import struct
from typing import Dict, Any, Optional, List
from .base import DataTypeDecoder, DecodedPayload
class TSPICTSDecoder(DataTypeDecoder):
"""Base decoder for TSPI/CTS data types (0x70-0x77)"""
def __init__(self):
super().__init__()
self.data_type_base = 0x70
self.data_type_name = "TSPI/CTS"
self.supported_formats = list(range(0x70, 0x78))
def can_decode(self, data_type: int) -> bool:
return 0x70 <= data_type <= 0x77
def get_data_type_name(self, data_type: int) -> str:
format_names = {
0x70: "GPS NMEA-RTCM",
0x71: "EAG ACMI",
0x72: "ACTTS",
0x73: "TSPI/CTS Format 3",
0x74: "TSPI/CTS Format 4",
0x75: "TSPI/CTS Format 5",
0x76: "TSPI/CTS Format 6",
0x77: "TSPI/CTS Format 7"
}
return format_names.get(data_type, f"TSPI/CTS Format {data_type & 0x0F}")
def decode(self, payload: bytes, ch10_header: Dict[str, Any]) -> Optional[DecodedPayload]:
"""Decode TSPI/CTS payload"""
data_type = ch10_header.get('data_type', 0)
if not self.can_decode(data_type):
return None
# Parse based on specific format
if data_type == 0x70:
return self._decode_gps_nmea(payload, ch10_header)
elif data_type == 0x71:
return self._decode_eag_acmi(payload, ch10_header)
elif data_type == 0x72:
return self._decode_actts(payload, ch10_header)
else:
return self._decode_generic_tspi(payload, ch10_header)
def _decode_gps_nmea(self, payload: bytes, ch10_header: Dict[str, Any]) -> DecodedPayload:
"""Decode GPS NMEA-RTCM data (Format 0)"""
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# NMEA messages are typically ASCII text
if data_start < len(payload):
nmea_data = payload[data_start:]
# Try to decode as ASCII
try:
nmea_text = nmea_data.decode('ascii').strip()
decoded_data['nmea_messages'] = nmea_text.split('\n')
decoded_data['message_count'] = len(decoded_data['nmea_messages'])
# Parse individual NMEA sentences
parsed_sentences = []
for sentence in decoded_data['nmea_messages']:
if sentence.startswith('$'):
parsed_sentences.append(self._parse_nmea_sentence(sentence))
decoded_data['parsed_sentences'] = parsed_sentences
except UnicodeDecodeError:
decoded_data['raw_nmea_data'] = nmea_data.hex()
errors.append("Failed to decode NMEA data as ASCII")
return DecodedPayload(
data_type=0x70,
data_type_name="GPS NMEA-RTCM",
format_version=0,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'TSPICTSDecoder'}
)
def _decode_eag_acmi(self, payload: bytes, ch10_header: Dict[str, Any]) -> DecodedPayload:
"""Decode EAG ACMI data (Format 1)"""
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# EAG ACMI format parsing
if data_start + 16 <= len(payload):
# ACMI header structure (simplified)
acmi_header = self._safe_unpack('<IIII', payload, data_start)
if acmi_header:
decoded_data.update({
'acmi_time_tag': acmi_header[0],
'acmi_entity_id': acmi_header[1],
'acmi_message_type': acmi_header[2],
'acmi_data_length': acmi_header[3]
})
# Parse ACMI data payload
acmi_data_start = data_start + 16
acmi_data_end = min(acmi_data_start + acmi_header[3], len(payload))
decoded_data['acmi_payload'] = payload[acmi_data_start:acmi_data_end]
else:
errors.append("Failed to parse ACMI header")
else:
errors.append("Insufficient data for ACMI header")
return DecodedPayload(
data_type=0x71,
data_type_name="EAG ACMI",
format_version=1,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'TSPICTSDecoder'}
)
def _decode_actts(self, payload: bytes, ch10_header: Dict[str, Any]) -> DecodedPayload:
"""Decode ACTTS (Advanced Common Time & Test System) data (Format 2)"""
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# ACTTS timing format
if data_start + 24 <= len(payload):
# ACTTS header structure
actts_data = self._safe_unpack('<QIIIIII', payload, data_start)
if actts_data:
decoded_data.update({
'actts_time_reference': actts_data[0], # 64-bit time reference
'actts_time_format': actts_data[1], # Time format indicator
'actts_clock_source': actts_data[2], # Clock source ID
'actts_sync_status': actts_data[3], # Synchronization status
'actts_time_quality': actts_data[4], # Time quality indicator
'actts_reserved': actts_data[5] # Reserved field
})
# Decode time format
time_format = actts_data[1]
decoded_data['time_format_description'] = self._decode_time_format(time_format)
# Decode sync status
sync_status = actts_data[3]
decoded_data['sync_status_description'] = self._decode_sync_status(sync_status)
# Parse additional ACTTS data if present
additional_data_start = data_start + 24
if additional_data_start < len(payload):
additional_data = payload[additional_data_start:]
decoded_data['additional_timing_data'] = additional_data.hex()
decoded_data['additional_data_length'] = len(additional_data)
else:
errors.append("Failed to parse ACTTS header")
else:
errors.append("Insufficient data for ACTTS header")
return DecodedPayload(
data_type=0x72,
data_type_name="ACTTS",
format_version=2,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'TSPICTSDecoder'}
)
def _decode_generic_tspi(self, payload: bytes, ch10_header: Dict[str, Any]) -> DecodedPayload:
"""Decode generic TSPI/CTS data (Formats 3-7)"""
data_type = ch10_header.get('data_type', 0)
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# Generic parsing for unknown formats
if data_start < len(payload):
remaining_data = payload[data_start:]
decoded_data['raw_data'] = remaining_data.hex()
decoded_data['data_length'] = len(remaining_data)
# Try to identify patterns
if len(remaining_data) >= 4:
# Check for timing patterns
potential_timestamps = []
for i in range(0, min(len(remaining_data) - 4, 64), 4):
timestamp = struct.unpack('<I', remaining_data[i:i+4])[0]
potential_timestamps.append(timestamp)
decoded_data['potential_timestamps'] = potential_timestamps[:16] # Limit output
return DecodedPayload(
data_type=data_type,
data_type_name=self.get_data_type_name(data_type),
format_version=data_type & 0x0F,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'TSPICTSDecoder'}
)
def _parse_nmea_sentence(self, sentence: str) -> Dict[str, Any]:
"""Parse individual NMEA sentence"""
if not sentence.startswith('$') or '*' not in sentence:
return {'raw': sentence, 'valid': False}
# Split sentence and checksum
parts = sentence.split('*')
if len(parts) != 2:
return {'raw': sentence, 'valid': False}
data_part = parts[0][1:] # Remove '$'
checksum = parts[1]
# Parse data fields
fields = data_part.split(',')
sentence_type = fields[0] if fields else ''
return {
'raw': sentence,
'sentence_type': sentence_type,
'fields': fields,
'checksum': checksum,
'valid': True
}
def _decode_time_format(self, time_format: int) -> str:
"""Decode ACTTS time format field"""
formats = {
0: "UTC",
1: "GPS Time",
2: "Local Time",
3: "Mission Time",
4: "Relative Time"
}
return formats.get(time_format, f"Unknown ({time_format})")
def _decode_sync_status(self, sync_status: int) -> str:
"""Decode ACTTS synchronization status"""
status_bits = {
0x01: "Time Valid",
0x02: "Sync Locked",
0x04: "External Reference",
0x08: "High Accuracy",
0x10: "Leap Second Pending"
}
active_flags = []
for bit, description in status_bits.items():
if sync_status & bit:
active_flags.append(description)
return ", ".join(active_flags) if active_flags else "No Status"
# Specific decoder instances
class ACTTSDecoder(TSPICTSDecoder):
"""Dedicated ACTTS decoder"""
def can_decode(self, data_type: int) -> bool:
return data_type == 0x72
class GPSNMEADecoder(TSPICTSDecoder):
"""Dedicated GPS NMEA decoder"""
def can_decode(self, data_type: int) -> bool:
return data_type == 0x70
class EAGACMIDecoder(TSPICTSDecoder):
"""Dedicated EAG ACMI decoder"""
def can_decode(self, data_type: int) -> bool:
return data_type == 0x71

View File

@@ -0,0 +1,69 @@
"""
UART Data decoder for Chapter 10 data types
Supports UART Data Format 0 (0x50)
"""
import struct
from typing import Dict, Any, Optional
from .base import DataTypeDecoder, DecodedPayload
class UARTDecoder(DataTypeDecoder):
"""Decoder for UART Data type (0x50)"""
def __init__(self):
super().__init__()
self.data_type_base = 0x50
self.data_type_name = "UART Data"
self.supported_formats = [0x50]
def can_decode(self, data_type: int) -> bool:
return data_type == 0x50
def get_data_type_name(self, data_type: int) -> str:
return "UART Data Format 0"
def decode(self, payload: bytes, ch10_header: Dict[str, Any]) -> Optional[DecodedPayload]:
"""Decode UART payload"""
decoded_data = {}
errors = []
# Parse IPH
iph = self._parse_intra_packet_header(payload)
if iph:
decoded_data.update(iph)
data_start = iph['data_start']
else:
data_start = 0
errors.append("Failed to parse intra-packet header")
# Parse UART data
if data_start < len(payload):
uart_data = payload[data_start:]
decoded_data['uart_data_length'] = len(uart_data)
# Try to decode as text
try:
text_data = uart_data.decode('ascii', errors='ignore')
decoded_data['ascii_data'] = text_data
decoded_data['printable_chars'] = sum(1 for c in text_data if c.isprintable())
except:
decoded_data['ascii_decode_failed'] = True
# Raw hex representation
decoded_data['raw_hex'] = uart_data[:256].hex() # First 256 bytes
# Basic statistics
decoded_data['null_count'] = uart_data.count(0)
decoded_data['cr_count'] = uart_data.count(ord('\r'))
decoded_data['lf_count'] = uart_data.count(ord('\n'))
return DecodedPayload(
data_type=0x50,
data_type_name="UART Data Format 0",
format_version=0,
decoded_data=decoded_data,
raw_payload=payload,
errors=errors,
metadata={'decoder': 'UARTDecoder'}
)