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