From 2cfb0c1342cd27cc38b9a640876ad83f14eceff5 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Wed, 25 Jun 2025 14:00:15 +0200 Subject: [PATCH] add another 1553 data extract --- config/config.json | 87 +++++++++++++++++- radar_data_reader/core/data_structures.py | 85 ++++++++++++++++-- radar_data_reader/core/struct_parser.py | 102 +++++++++++----------- 3 files changed, 217 insertions(+), 57 deletions(-) diff --git a/config/config.json b/config/config.json index 4e047ed..0873bb5 100644 --- a/config/config.json +++ b/config/config.json @@ -3,7 +3,7 @@ "last_opened_rec_file": "C:/src/____GitProjects/radar_data_reader/_rec/_25-05-15-12-22-52_sata_345.rec", "last_out_output_dir": "C:/src/____GitProjects/radar_data_reader/_rec", "last_rec_output_dir": "C:\\src\\____GitProjects\\radar_data_reader\\_rec", - "active_out_export_profile_name": "prova1", + "active_out_export_profile_name": "gsp_data", "export_profiles": [ { "name": "prova1", @@ -152,6 +152,51 @@ "column_name": "target_detections", "data_path": "cdp_sts_results.payload.data.detections_chunk.data.target_detections", "translate_with_enum": false + }, + { + "column_name": "magnetic_heading_deg", + "data_path": "d1553_data.magnetic_heading_deg", + "translate_with_enum": false + }, + { + "column_name": "mission_timestamp_str", + "data_path": "d1553_data.mission_timestamp_str", + "translate_with_enum": false + }, + { + "column_name": "platform_azimuth_deg", + "data_path": "d1553_data.platform_azimuth_deg", + "translate_with_enum": false + }, + { + "column_name": "master_mode", + "data_path": "main_header.ge_header.mode.master_mode", + "translate_with_enum": true + }, + { + "column_name": "operation_mode", + "data_path": "main_header.ge_header.mode.operation_mode", + "translate_with_enum": true + }, + { + "column_name": "radiate", + "data_path": "main_header.ge_header.mode.radiate", + "translate_with_enum": true + }, + { + "column_name": "standby", + "data_path": "main_header.ge_header.mode.standby", + "translate_with_enum": true + }, + { + "column_name": "range_scale", + "data_path": "main_header.ge_header.mode.range_scale", + "translate_with_enum": true + }, + { + "column_name": "scan_rate", + "data_path": "main_header.ge_header.mode.scan_rate", + "translate_with_enum": true } ] }, @@ -163,11 +208,36 @@ "data_path": "batch_id", "translate_with_enum": false }, + { + "column_name": "batch_counter", + "data_path": "main_header.ge_header.signal_descr.batch_counter", + "translate_with_enum": false + }, + { + "column_name": "timetag", + "data_path": "main_header.ge_header.general_settings.mission_time.timetag", + "translate_with_enum": false + }, + { + "column_name": "ttag", + "data_path": "main_header.ge_header.signal_descr.ttag", + "translate_with_enum": false + }, + { + "column_name": "mission_timestamp_str", + "data_path": "d1553_data.mission_timestamp_str", + "translate_with_enum": false + }, { "column_name": "master_mode", "data_path": "main_header.ge_header.mode.master_mode", "translate_with_enum": false }, + { + "column_name": "operation_mode", + "data_path": "main_header.ge_header.mode.operation_mode", + "translate_with_enum": false + }, { "column_name": "range_scale", "data_path": "main_header.ge_header.mode.range_scale", @@ -257,6 +327,21 @@ "column_name": "true_heading_deg", "data_path": "d1553_data.true_heading_deg", "translate_with_enum": false + }, + { + "column_name": "platform_azimuth_deg", + "data_path": "d1553_data.platform_azimuth_deg", + "translate_with_enum": false + }, + { + "column_name": "magnetic_heading_deg", + "data_path": "d1553_data.magnetic_heading_deg", + "translate_with_enum": false + }, + { + "column_name": "ground_track_angle_deg", + "data_path": "d1553_data.ground_track_angle_deg", + "translate_with_enum": false } ] } diff --git a/radar_data_reader/core/data_structures.py b/radar_data_reader/core/data_structures.py index 26cf32e..8a1fc71 100644 --- a/radar_data_reader/core/data_structures.py +++ b/radar_data_reader/core/data_structures.py @@ -6,10 +6,15 @@ binary layout of the radar data file's C/C++ structs. """ import ctypes +import math from typing import List, Optional, Tuple, Union from dataclasses import dataclass, field import numpy as np +from ..utils import logger + +log = logger.get_logger(__name__) + # --- Base Block (Python-side Representation, not ctypes) --- @dataclass @@ -695,25 +700,95 @@ class D1553Block(BaseBlock): def true_heading_deg(self) -> Optional[float]: if not (self.is_valid and self.payload): return None - return ctypes.c_int16(self.payload.d.a4[2]).value * ICD1553_SEMICIRCLE_DEG_LSB + raw_val = self.payload.d.a4[2] + return ctypes.c_int16(raw_val).value * ICD1553_SEMICIRCLE_DEG_LSB + + @property + def magnetic_heading_deg(self) -> Optional[float]: + if not (self.is_valid and self.payload): + return None + # From C++: ((signed short)x->a4[3])*ICD1553_SEMICIRCLE_DEG_LSB + raw_val = self.payload.d.a4[3] + return ctypes.c_int16(raw_val).value * ICD1553_SEMICIRCLE_DEG_LSB + + @property + def platform_azimuth_deg(self) -> Optional[float]: + if not (self.is_valid and self.payload): + return None + # From C++: ((signed short)x->a5[8])*ICD1553_SEMICIRCLE_DEG_LSB + raw_val = self.payload.d.a5[8] + return ctypes.c_int16(raw_val).value * ICD1553_SEMICIRCLE_DEG_LSB @property def north_velocity_ms(self) -> Optional[float]: if not (self.is_valid and self.payload): return None - return ctypes.c_int16(self.payload.d.a4[10]).value * ICD1553_VELOCITY_METERS_LSB + # The C++ uses w2f, which combines two words. Replicating that logic. + raw_val = (self.payload.d.a5[2] << 16) | self.payload.d.a5[3] + return ctypes.c_int32(raw_val).value * ICD1553_VELOCITY_METERS_LSB @property def east_velocity_ms(self) -> Optional[float]: if not (self.is_valid and self.payload): return None - return ctypes.c_int16(self.payload.d.a4[11]).value * ICD1553_VELOCITY_METERS_LSB + raw_val = (self.payload.d.a5[4] << 16) | self.payload.d.a5[5] + return ctypes.c_int32(raw_val).value * ICD1553_VELOCITY_METERS_LSB @property def down_velocity_ms(self) -> Optional[float]: if not (self.is_valid and self.payload): return None - return ctypes.c_int16(self.payload.d.a4[12]).value * ICD1553_VELOCITY_METERS_LSB + raw_val = (self.payload.d.a5[6] << 16) | self.payload.d.a5[7] + return ctypes.c_int32(raw_val).value * ICD1553_VELOCITY_METERS_LSB + + @property + def ground_track_angle_deg(self) -> Optional[float]: + """Calculated from north and east velocities.""" + if not (self.is_valid and self.payload): + return None + + # Requires north_velocity_ms and east_velocity_ms to be valid + vx = self.east_velocity_ms + vy = self.north_velocity_ms + + if vx is None or vy is None: + return None + + # From C++: atan2(vy, vx) gives angle from East, atan2(vx, vy) from North + # The C++ code `atan2(vx,vy)` calculates angle from North. + track_rad = math.atan2(vx, vy) + return math.degrees(track_rad) + + @property + def mission_timestamp_str(self) -> Optional[str]: + if not (self.is_valid and self.payload): + return None + + try: + # From C++: date=x->a1[5] and time=x->a1[6] + date_word = self.payload.d.a1[5] + time_word = self.payload.d.a1[6] + + # Decoding date (emulating the C++ logic, including the fixed year) + # This seems specific and might need review if data from other years is used + year = 2022 + month = (date_word >> 6) & 0xF + day = (date_word >> 10) & 0x3F + + # Decoding time + total_seconds = time_word * 2 + h = total_seconds // 3600 + m = (total_seconds % 3600) // 60 + s = total_seconds % 60 + + # Basic validation of parsed values + if not (1 <= month <= 12 and 1 <= day <= 31 and 0 <= h <= 23 and 0 <= m <= 59 and 0 <= s <= 59): + log.warning(f"Invalid mission date/time decoded: Y={year}, M={month}, D={day}, H={h}, m={m}, s={s}") + return "Invalid DateTime" + + return f"{year:04d}-{month:02d}-{day:02d}T{h:02d}:{m:02d}:{s:02d}" + except (IndexError, TypeError): + return None # Generic block dataclasses (defined AFTER ALL ctypes structs) @@ -767,4 +842,4 @@ class DataBatch: for block in self.blocks: if isinstance(block, DspHeaderIn): return block - return None + return None \ No newline at end of file diff --git a/radar_data_reader/core/struct_parser.py b/radar_data_reader/core/struct_parser.py index 5ed295e..fcf25b7 100644 --- a/radar_data_reader/core/struct_parser.py +++ b/radar_data_reader/core/struct_parser.py @@ -103,7 +103,46 @@ def _parse_timer_block( block_name=block_name, block_size_words=block_size_words, is_valid=False ) +def _parse_avionics_payload( + block_data_bytes: bytes, block_name: str, block_size_words: int +) -> ds.D1553Block: + """ + Parses a buffer known to contain an avionics payload (like MIL-STD-1553 data). + This payload has a 144-byte header that must be skipped. + """ + # From C++ analysis, the actual payload data starts after a 36-word (144-byte) header. + AVIONICS_PAYLOAD_OFFSET_BYTES = 36 * 4 + required_size = AVIONICS_PAYLOAD_OFFSET_BYTES + ctypes.sizeof(ds.D1553Payload) + actual_size = len(block_data_bytes) + + if actual_size < required_size: + log.warning( + f"{block_name} block is too small for the avionics payload with offset. " + f"Size: {actual_size}, Required: {required_size}." + ) + return ds.D1553Block( + block_name=block_name, block_size_words=block_size_words, is_valid=False + ) + + try: + # Apply the offset to skip the header before mapping the structure + payload = ds.D1553Payload.from_buffer_copy( + block_data_bytes, AVIONICS_PAYLOAD_OFFSET_BYTES + ) + log.debug(f"Parsed {block_name} with offset {AVIONICS_PAYLOAD_OFFSET_BYTES}.") + return ds.D1553Block( + block_name=block_name, + block_size_words=block_size_words, + is_valid=True, + payload=payload, + ) + except Exception as e: + log.error(f"Failed to map data to D1553Payload from {block_name}: {e}", exc_info=True) + return ds.D1553Block( + block_name=block_name, block_size_words=block_size_words, is_valid=False + ) + # --- Funzione di parsing AESA --- def _parse_aesa_block( block_data_bytes: bytes, block_name: str, block_size_words: int @@ -215,38 +254,8 @@ def _parse_aesa_block( def _parse_d1553_block( block_data_bytes: bytes, block_name: str, block_size_words: int ) -> ds.D1553Block: - - # IPOTESI: Applichiamo lo stesso offset visto per i blocchi SOFT - # anche ai blocchi D1553 nativi. - d1553_data_offset_bytes = 36 * 4 - - # Controlliamo se il blocco è abbastanza grande per contenere l'offset E il payload - required_size = d1553_data_offset_bytes + ctypes.sizeof(ds.D1553Payload) - actual_size = len(block_data_bytes) - - if actual_size < required_size: - log.warning( - f"{block_name} block is too small for D1553Payload with offset. Size: {actual_size}, Required: {required_size}." - ) - return ds.D1553Block( - block_name=block_name, block_size_words=block_size_words, is_valid=False - ) - - try: - # Applichiamo l'offset prima di mappare la struttura - payload = ds.D1553Payload.from_buffer_copy(block_data_bytes, d1553_data_offset_bytes) - log.debug(f"Parsed {block_name} with offset {d1553_data_offset_bytes}.") - return ds.D1553Block( - block_name=block_name, - block_size_words=block_size_words, - is_valid=True, - payload=payload, - ) - except Exception as e: - log.error(f"Failed to map data to D1553Payload from {block_name}: {e}", exc_info=True) - return ds.D1553Block( - block_name=block_name, block_size_words=block_size_words, is_valid=False - ) + """Handles native D1553 blocks by parsing their avionics payload.""" + return _parse_avionics_payload(block_data_bytes, block_name, block_size_words) # --- Funzioni di parsing EXP, PC, DET (con gestione raw_data_bytes) --- @@ -446,30 +455,21 @@ def parse_block( return _parse_d1553_block(block_data_bytes, block_name, block_size_words) elif block_name == "SOFT": # Per C++: if ((p[17]==0x35353144) && (p[18]==0x00000033)) - # d1553_extract(p, gps_save_mode); - # Questo blocco SOFT contiene dati 1553 + # This SOFT block contains 1553-like data. if block_size_words > 18: marker1 = block_data_numpy[17] marker2 = block_data_numpy[18] if marker1 == 0x35353144 and marker2 == 0x00000033: - log.debug( - f"SOFT block at offset contains 1553 data. Parsing as D1553." + log.debug("SOFT block contains avionics data. Parsing.") + # Let the dedicated avionics payload parser handle it. + return _parse_avionics_payload( + block_data_bytes, "D1553_from_SOFT", block_size_words ) - # Il codice C++ passa l'intero puntatore, ma la funzione - # d1553_extract applica un offset di 36 parole. - # Applichiamo lo stesso offset qui. - d1553_data_offset_bytes = 36 * 4 - if len(block_data_bytes) > d1553_data_offset_bytes: - return _parse_d1553_block( - block_data_bytes[d1553_data_offset_bytes:], - "D1553_from_SOFT", # Usiamo un nome per tracciarlo - (len(block_data_bytes) - d1553_data_offset_bytes) // 4, - ) - else: - log.warning("SOFT block with 1553 markers is too small for payload.") - - # Se non è un blocco SOFT con dati 1553, trattalo come generico - return ds.GenericBlock(block_name=block_name, block_size_words=block_size_words) + + # If it's not the specific SOFT block with avionics, treat as generic. + return ds.GenericBlock( + block_name=block_name, block_size_words=block_size_words + ) elif block_name == "EXP": return _parse_exp_block(block_data_bytes, block_name, block_size_words) elif block_name == "PC":