pretty good
This commit is contained in:
315
analyzer/protocols/decoders/tspi_cts.py
Normal file
315
analyzer/protocols/decoders/tspi_cts.py
Normal file
@@ -0,0 +1,315 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user