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

315 lines
12 KiB
Python

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