add another 1553 data extract

This commit is contained in:
VALLONGOL 2025-06-25 14:00:15 +02:00
parent 39c950ded4
commit 2cfb0c1342
3 changed files with 217 additions and 57 deletions

View File

@ -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
}
]
}

View File

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

View File

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