279 lines
10 KiB
Python
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 |