315 lines
12 KiB
Python
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 |