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

279 lines
10 KiB
Python

"""
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