fix d1553 struct

This commit is contained in:
VALLONGOL 2025-06-25 15:22:14 +02:00
parent f9d42677ac
commit db0e26b5a1
4 changed files with 161 additions and 233 deletions

View File

@ -197,6 +197,21 @@
"column_name": "scan_rate", "column_name": "scan_rate",
"data_path": "main_header.ge_header.mode.scan_rate", "data_path": "main_header.ge_header.mode.scan_rate",
"translate_with_enum": true "translate_with_enum": true
},
{
"column_name": "ground_track_angle_deg",
"data_path": "d1553_data.ground_track_angle_deg",
"translate_with_enum": false
},
{
"column_name": "range_scale_nm",
"data_path": "d1553_data.range_scale_nm",
"translate_with_enum": false
},
{
"column_name": "raw_mode",
"data_path": "d1553_data.raw_mode",
"translate_with_enum": true
} }
] ]
}, },
@ -342,6 +357,11 @@
"column_name": "ground_track_angle_deg", "column_name": "ground_track_angle_deg",
"data_path": "d1553_data.ground_track_angle_deg", "data_path": "d1553_data.ground_track_angle_deg",
"translate_with_enum": false "translate_with_enum": false
},
{
"column_name": "raw_mode",
"data_path": "d1553_data.raw_mode",
"translate_with_enum": true
} }
] ]
} }

View File

@ -222,4 +222,6 @@ ENUM_REGISTRY = {
"cdp_sts_results.payload.data.detections_chunk.data.detected_in_lookup": Boolean, "cdp_sts_results.payload.data.detections_chunk.data.detected_in_lookup": Boolean,
# AESA Block -> Synthetic Report # AESA Block -> Synthetic Report
"aesa_data.payload.comm": AesaError, "aesa_data.payload.comm": AesaError,
# D1553
"d1553_data.raw_mode": MasterMode,
} }

View File

@ -235,3 +235,26 @@ class D1553Block(BaseBlock):
def raw_range_scale(self) -> Optional[int]: def raw_range_scale(self) -> Optional[int]:
self._ensure_cache() self._ensure_cache()
return self._cache.raw_range_scale return self._cache.raw_range_scale
@property
def raw_mode(self) -> Optional[int]:
"""
Returns the raw master mode value from the B7 message.
Note: The C++ code uses a global r_mode, which seems to come
from the DSPHDRIN. This property exposes the value potentially
present in the 1553 message itself for comparison.
The bits for this are an assumption based on typical avionics.
Let's assume it's in b7[0], high bits.
"""
self._ensure_cache()
try:
# This is an assumption, needs validation with real data.
# From C++: unsigned int mode=x->b7[0]>>12;
return (self.payload.d.b7[0] >> 12)
except (TypeError, IndexError):
return None
@property
def raw_range_scale(self) -> Optional[int]:
self._ensure_cache()
return self._cache.raw_range_scale

View File

@ -5,7 +5,7 @@ from tkinter import ttk, simpledialog, messagebox
import ctypes import ctypes
import copy import copy
import re import re
import inspect # Import necessary for inspecting properties import inspect
from typing import List, Type, Dict, Any, Optional from typing import List, Type, Dict, Any, Optional
from .gui_utils import center_window from .gui_utils import center_window
@ -76,6 +76,7 @@ class ProfileEditorWindow(tk.Toplevel):
self._init_window() self._init_window()
self._init_vars() self._init_vars()
self._create_widgets() self._create_widgets()
# Initial population of the tree
self._populate_available_fields_tree() self._populate_available_fields_tree()
self._load_profiles_to_combobox() self._load_profiles_to_combobox()
center_window(self, self.master) center_window(self, self.master)
@ -118,6 +119,8 @@ class ProfileEditorWindow(tk.Toplevel):
fields_frame.columnconfigure(0, weight=1) fields_frame.columnconfigure(0, weight=1)
self.fields_tree = ttk.Treeview(fields_frame, selectmode="browse") self.fields_tree = ttk.Treeview(fields_frame, selectmode="browse")
self.fields_tree.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) self.fields_tree.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
# --- New Style Tag for used fields ---
self.fields_tree.tag_configure("used_field", foreground="gray", font=("", 0, "italic"))
ysb = ttk.Scrollbar( ysb = ttk.Scrollbar(
fields_frame, orient="vertical", command=self.fields_tree.yview fields_frame, orient="vertical", command=self.fields_tree.yview
) )
@ -155,6 +158,10 @@ class ProfileEditorWindow(tk.Toplevel):
show="headings", show="headings",
selectmode="browse", selectmode="browse",
) )
# --- New Style Tags for translatable cells ---
self.selected_tree.tag_configure("translatable", foreground="#007acc")
self.selected_tree.tag_configure("not_translatable", foreground="black")
self.selected_tree.heading("display_name", text="Field Name") self.selected_tree.heading("display_name", text="Field Name")
self.selected_tree.heading("data_path", text="Source Path") self.selected_tree.heading("data_path", text="Source Path")
self.selected_tree.heading("translate", text="Translate") self.selected_tree.heading("translate", text="Translate")
@ -192,7 +199,7 @@ class ProfileEditorWindow(tk.Toplevel):
dialog = EditPathDialog(self, initial_value=field.data_path) dialog = EditPathDialog(self, initial_value=field.data_path)
new_path = dialog.wait_for_input() new_path = dialog.wait_for_input()
if new_path is not None and new_path != field.data_path: if new_path is not None and new_path != field.data_path:
if not re.match(r"^[\w\.\[\]]+$", new_path): if not re.match(r"^[\w\.\[\]_]+$", new_path):
messagebox.showerror( messagebox.showerror(
"Invalid Path", "The path contains invalid characters.", parent=self "Invalid Path", "The path contains invalid characters.", parent=self
) )
@ -205,7 +212,7 @@ class ProfileEditorWindow(tk.Toplevel):
if region != "cell": if region != "cell":
return return
column_id = self.selected_tree.identify_column(event.x) column_id = self.selected_tree.identify_column(event.x)
if column_id != "#3": if column_id != "#3": # Only act on the "Translate" column
return return
item_id = self.selected_tree.identify_row(event.y) item_id = self.selected_tree.identify_row(event.y)
if not item_id: if not item_id:
@ -218,26 +225,33 @@ class ProfileEditorWindow(tk.Toplevel):
base_path = re.sub(r"\[\d+\]", "", field.data_path) base_path = re.sub(r"\[\d+\]", "", field.data_path)
if base_path in ENUM_REGISTRY: if base_path in ENUM_REGISTRY:
field.translate_with_enum = not field.translate_with_enum field.translate_with_enum = not field.translate_with_enum
self._load_profile_into_ui() self._load_profile_into_ui() # Redraw to update checkbox
def _load_profile_into_ui(self): def _load_profile_into_ui(self):
# 1. Update the "Selected Fields" tree
for i in self.selected_tree.get_children(): for i in self.selected_tree.get_children():
self.selected_tree.delete(i) self.selected_tree.delete(i)
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile: if not profile:
self._update_available_fields_tree_tags() # Still update tags for empty profile
return return
for index, field in enumerate(profile.fields): for index, field in enumerate(profile.fields):
base_path = re.sub(r"\[\d+\]", "", field.data_path) base_path = re.sub(r"\[\d+\]", "", field.data_path)
is_translatable = base_path in ENUM_REGISTRY is_translatable = base_path in ENUM_REGISTRY
checkbox_char = ( checkbox_char = "" if field.translate_with_enum else ""
"" if is_translatable and field.translate_with_enum else ""
) tag = "translatable" if is_translatable else "not_translatable"
self.selected_tree.insert( self.selected_tree.insert(
"", "",
"end", "end",
iid=str(index), iid=str(index),
values=(field.column_name, field.data_path, checkbox_char), values=(field.column_name, field.data_path, checkbox_char),
tags=(tag,)
) )
# 2. Update the "Available Fields" tree to show used fields
self._update_available_fields_tree_tags()
def _clear_selected_fields(self): def _clear_selected_fields(self):
profile = self._get_current_profile() profile = self._get_current_profile()
@ -254,183 +268,57 @@ class ProfileEditorWindow(tk.Toplevel):
def _populate_available_fields_tree(self): def _populate_available_fields_tree(self):
self.fields_tree.delete(*self.fields_tree.get_children()) self.fields_tree.delete(*self.fields_tree.get_children())
# --- Batch Properties (standalone fields) --- # --- Helper to create a non-selectable root node ---
batch_root = self.fields_tree.insert( def add_virtual_root(iid, text):
"", "end", iid="batch_properties", text="Batch Properties" return self.fields_tree.insert("", "end", iid=iid, text=text, open=False)
)
self.fields_tree.insert( # --- Batch Properties ---
batch_root, batch_root = add_virtual_root("batch_properties", "Batch Properties")
"end", self._recursive_populate_properties(ds.DataBatch, batch_root, "", ["blocks", "cdp_sts_results", "timer_data", "aesa_data", "d1553_data", "exp_data", "pc_data", "det_data", "main_header"])
iid="batch_id",
text="batch_id",
values=("batch_id", "batch_id"),
)
# --- DSPHDRIN Data --- # --- DSPHDRIN Data ---
header_root = self.fields_tree.insert( header_root = add_virtual_root("header_data", "Header Data (from DSPHDRIN)")
"", "end", iid="header_data", text="Header Data (from DSPHDRIN)" self._recursive_populate_tree_ctypes(ds.GeHeader, header_root, "main_header.ge_header")
)
self._recursive_populate_tree_ctypes(
ds.GeHeader, header_root, "main_header.ge_header"
)
# --- CDPSTS Data --- # --- CDPSTS Data ---
cdpsts_root = self.fields_tree.insert( cdpsts_root = add_virtual_root("cdpsts_data", "CDP/STS Block Data")
"", "end", iid="cdpsts_data", text="CDP/STS Block Data" self._recursive_populate_tree_ctypes(ds.CdpStsPayload, cdpsts_root, "cdp_sts_results.payload")
)
self._recursive_populate_tree_ctypes( # --- D1553 Data (with visual grouping) ---
ds.CdpDataLayout, cdpsts_root, "cdp_sts_results.payload.data" d1553_root = add_virtual_root("d1553_data", "D1553 Block Data")
) d1553_nav_root = self.fields_tree.insert(d1553_root, "end", iid="d1553_nav", text="Navigation Data")
d1553_status_root = self.fields_tree.insert(d1553_root, "end", iid="d1553_status", text="Status Data")
nav_props = ["mission_timestamp_str", "latitude_deg", "longitude_deg", "baro_altitude_m", "true_heading_deg", "magnetic_heading_deg", "platform_azimuth_deg", "north_velocity_ms", "east_velocity_ms", "down_velocity_ms", "ground_track_angle_deg"]
status_props = ["range_scale_nm", "raw_range_scale", "raw_mode"]
self._recursive_populate_properties(ds.D1553Block, d1553_nav_root, "d1553_data", only_props=nav_props)
self._recursive_populate_properties(ds.D1553Block, d1553_status_root, "d1553_data", only_props=status_props)
# --- TIMER Data --- # --- TIMER Data ---
timer_root = self.fields_tree.insert( timer_root = add_virtual_root("timer_data", "Timer Block Data")
"", "end", iid="timer_data", text="Timer Block Data" self._recursive_populate_tree_ctypes(ds.GrifoTimerBlob, timer_root, "timer_data.blob")
)
self._recursive_populate_tree_ctypes(
ds.GrifoTimerBlob, timer_root, "timer_data.blob"
)
# --- AESA Data --- # ... (add other blocks like AESA, EXP, etc. as needed, following this pattern) ...
aesa_root = self.fields_tree.insert(
"", "end", iid="aesa_data", text="AESA Block Data"
)
aesa_raw_data_root = self.fields_tree.insert(
aesa_root, "end", iid="aesa_raw_data", text="Raw Data By Subtype"
)
self.fields_tree.insert(
aesa_raw_data_root,
"end",
iid="aesa_raw_bytes",
text="raw_data_bytes",
values=("raw_data_bytes", "aesa_data.raw_data_bytes"),
)
aesa_synth_root = self.fields_tree.insert(
aesa_root, "end", iid="aesa_synth", text="Synthetic Report (256 bytes)"
)
self._recursive_populate_tree_ctypes(
ds.AesaSyntheticReport, aesa_synth_root, "aesa_data.payload"
)
# --- D1553 Data --- def _update_available_fields_tree_tags(self):
d1553_root = self.fields_tree.insert( profile = self._get_current_profile()
"", "end", iid="d1553_data", text="D1553 Block Data" used_paths = {f.data_path for f in profile.fields} if profile else set()
)
d1553_raw_root = self.fields_tree.insert(
d1553_root, "end", iid="d1553_payload", text="Raw Payload (D1553Payload)"
)
self._recursive_populate_tree_ctypes(
ds.D1553Payload, d1553_raw_root, "d1553_data.payload"
)
d1553_calc_root = self.fields_tree.insert(
d1553_root, "end", iid="d1553_calculated", text="Calculated Properties"
)
dummy_d1553_block = ds.D1553Block(
block_name="D1553", block_size_words=0, is_valid=False
) # Needs a valid (but dummy) instance
self._recursive_populate_properties(
type(dummy_d1553_block), d1553_calc_root, "d1553_data"
)
# --- EXPANDER Data --- for item_id in self._get_all_tree_children(self.fields_tree, ""):
exp_root = self.fields_tree.insert( item_values = self.fields_tree.item(item_id, "values")
"", "end", iid="exp_data", text="Expander Block Data" if item_values and len(item_values) >= 2:
) data_path = item_values[1]
# Add the raw_data_bytes field for Expander if data_path in used_paths:
self.fields_tree.insert( self.fields_tree.item(item_id, tags=("used_field",))
exp_root, else:
"end", self.fields_tree.item(item_id, tags=())
iid="exp_raw_bytes",
text="raw_data_bytes",
values=("raw_data_bytes", "exp_data.raw_data_bytes"),
)
# Explore the basic (miniaturized) payload
exp_basic_payload_root = self.fields_tree.insert(
exp_root, "end", iid="exp_basic_payload", text="Basic Payload (Mapped Part)"
)
self._recursive_populate_tree_ctypes(
ds.ExpRawIf, exp_basic_payload_root, "exp_data.payload"
)
# --- PC Data --- def _get_all_tree_children(self, tree, item):
pc_root = self.fields_tree.insert( children = tree.get_children(item)
"", "end", iid="pc_data", text="PC Block Data" for child in children:
) yield child
# Add the raw_data_bytes field for PC yield from self._get_all_tree_children(tree, child)
self.fields_tree.insert(
pc_root,
"end",
iid="pc_raw_bytes",
text="raw_data_bytes",
values=("raw_data_bytes", "pc_data.raw_data_bytes"),
)
# Explore the basic (miniaturized) payload
pc_basic_payload_root = self.fields_tree.insert(
pc_root, "end", iid="pc_basic_payload", text="Basic Payload (Mapped Part)"
)
self._recursive_populate_tree_ctypes(
ds.PcRawIf, pc_basic_payload_root, "pc_data.payload"
)
# --- DETECTOR Data ---
det_root = self.fields_tree.insert(
"", "end", iid="det_data", text="Detector Block Data"
)
# Add the raw_data_bytes field for Detector
self.fields_tree.insert(
det_root,
"end",
iid="det_raw_bytes",
text="raw_data_bytes",
values=("raw_data_bytes", "det_data.raw_data_bytes"),
)
# Explore the basic (miniaturized) payload
det_basic_payload_root = self.fields_tree.insert(
det_root, "end", iid="det_basic_payload", text="Basic Payload (Mapped Part)"
)
self._recursive_populate_tree_ctypes(
ds.DetectorRawIf, det_basic_payload_root, "det_data.payload"
)
def _recursive_populate_properties(
self, class_obj: Type[Any], parent_id: str, base_path: str
):
# We need an actual instance to safely call properties,
# but creating a dummy one for every class might be complex.
# Instead, we just list properties by name from the class.
for name in dir(class_obj):
if name.startswith("_"):
continue
attr = getattr(class_obj, name)
# Check if it's a property (getter method)
if isinstance(attr, property):
# Optionally, we can check if it's a function, though `property` implies it.
# if inspect.isfunction(attr.fget): # Check if it has a getter
# We can add a hint to the display name or values tuple if this property
# is known to derive from specific raw fields.
# For D1553, this would be: (name, f"{base_path}.{name} (from payload.d.a4[XX])")
# For now, let's keep it generic, just indicating it's a property.
display_text = f"{name} (Property)"
# A common convention for properties is to put relevant raw data paths in their docstring.
# We could try to read it here:
# if attr.__doc__:
# # Example docstring: "Calculates latitude from payload.d.a4[23] and a4[24]"
# # You could parse this docstring for the raw data path.
# pass
self.fields_tree.insert(
parent_id,
"end",
iid=f"{parent_id}_{name}",
text=display_text,
values=(
name,
f"{base_path}.{name}",
), # The path is directly to the property
)
def _recursive_populate_tree_ctypes( def _recursive_populate_tree_ctypes(
self, class_obj: Type[ctypes.Structure], parent_id: str, base_path: str self, class_obj: Type[ctypes.Structure], parent_id: str, base_path: str
@ -440,31 +328,42 @@ class ProfileEditorWindow(tk.Toplevel):
for field_name, field_type in class_obj._fields_: for field_name, field_type in class_obj._fields_:
current_path = f"{base_path}.{field_name}" current_path = f"{base_path}.{field_name}"
node_id = f"{parent_id}_{field_name}" node_id = f"{parent_id}_{field_name}"
if hasattr(field_type, "_fields_"): if hasattr(field_type, "_fields_"): # It's a nested structure
child_node = self.fields_tree.insert( child_node = self.fields_tree.insert(
parent_id, "end", iid=node_id, text=field_name parent_id, "end", iid=node_id, text=field_name, open=False
) )
self._recursive_populate_tree_ctypes( self._recursive_populate_tree_ctypes(
field_type, child_node, current_path field_type, child_node, current_path
) )
elif hasattr(field_type, "_length_"): elif hasattr(field_type, "_length_"): # It's an array
self.fields_tree.insert( self.fields_tree.insert(
parent_id, parent_id, "end", iid=node_id, text=f"{field_name} [Array]",
"end", values=(field_name, current_path)
iid=node_id,
text=f"{field_name} [Array]",
values=(field_name, current_path),
) )
else: else: # It's a simple field
display_text = f"{field_name}" display_text = f"{field_name}"
if current_path in ENUM_REGISTRY: if current_path in ENUM_REGISTRY:
display_text += " (Enum)" display_text += " (Enum)"
self.fields_tree.insert( self.fields_tree.insert(
parent_id, parent_id, "end", iid=node_id, text=display_text,
"end", values=(field_name, current_path)
iid=node_id, )
text=display_text,
values=(field_name, current_path), def _recursive_populate_properties(self, class_obj: Type[Any], parent_id: str, base_path: str, excluded_props: List[str] = [], only_props: Optional[List[str]] = None):
for name in dir(class_obj):
if name.startswith("_") or name in excluded_props:
continue
if only_props and name not in only_props:
continue
if isinstance(getattr(class_obj, name, None), property):
display_text = name
current_path = f"{base_path}.{name}"
if current_path in ENUM_REGISTRY:
display_text += " (Enum)"
self.fields_tree.insert(
parent_id, "end", iid=f"{parent_id}_{name}",
text=display_text, values=(name, current_path)
) )
def _load_profiles_to_combobox(self): def _load_profiles_to_combobox(self):
@ -472,6 +371,8 @@ class ProfileEditorWindow(tk.Toplevel):
self.profile_combobox["values"] = profile_names self.profile_combobox["values"] = profile_names
if profile_names: if profile_names:
self.selected_profile_name.set(profile_names[0]) self.selected_profile_name.set(profile_names[0])
else:
self.selected_profile_name.set("")
self._load_profile_into_ui() self._load_profile_into_ui()
def _get_current_profile(self) -> Optional[ExportProfile]: def _get_current_profile(self) -> Optional[ExportProfile]:
@ -483,64 +384,55 @@ class ProfileEditorWindow(tk.Toplevel):
def _add_field(self): def _add_field(self):
selected_item_id = self.fields_tree.focus() selected_item_id = self.fields_tree.focus()
if not selected_item_id: if not selected_item_id: return
return
item_values = self.fields_tree.item(selected_item_id, "values") item_values = self.fields_tree.item(selected_item_id, "values")
if not item_values or len(item_values) < 2: if not item_values or len(item_values) < 2:
messagebox.showinfo( messagebox.showinfo( "Cannot Add Field", "Please select a specific data field, not a category.", parent=self)
"Cannot Add Field", "Please select a specific data field.", parent=self
)
return return
tags = self.fields_tree.item(selected_item_id, "tags")
if "used_field" in tags:
messagebox.showinfo("Duplicate Field", "This field is already in the current profile.", parent=self)
return
column_name, data_path = item_values column_name, data_path = item_values
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile: if not profile: return
return
if any(f.data_path == data_path for f in profile.fields):
messagebox.showinfo(
"Duplicate Field", "This field is already in the profile.", parent=self
)
return
profile.fields.append(ExportField(column_name=column_name, data_path=data_path)) profile.fields.append(ExportField(column_name=column_name, data_path=data_path))
self._load_profile_into_ui() self._load_profile_into_ui()
def _remove_field(self): def _remove_field(self):
selection = self.selected_tree.selection() selection = self.selected_tree.selection()
if not selection: if not selection: return
return
index_to_remove = int(selection[0]) index_to_remove = int(selection[0])
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile: if not profile: return
return
del profile.fields[index_to_remove] del profile.fields[index_to_remove]
self._load_profile_into_ui() self._load_profile_into_ui()
def _move_field(self, direction: int): def _move_field(self, direction: int):
selection = self.selected_tree.selection() selection = self.selected_tree.selection()
if not selection: if not selection: return
return
index = int(selection[0]) index = int(selection[0])
new_index = index + direction new_index = index + direction
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile or not (0 <= new_index < len(profile.fields)): if not profile or not (0 <= new_index < len(profile.fields)): return
return
fields = profile.fields fields = profile.fields
fields.insert(new_index, fields.pop(index)) fields.insert(new_index, fields.pop(index))
self._load_profile_into_ui() self._load_profile_into_ui()
self.selected_tree.selection_set(str(new_index)) self.selected_tree.selection_set(str(new_index))
def _on_new_profile(self): def _on_new_profile(self):
name = simpledialog.askstring( name = simpledialog.askstring( "New Profile", "Enter a name for the new profile:", parent=self)
"New Profile", "Enter a name for the new profile:", parent=self if not name or not name.strip(): return
)
if not name or not name.strip():
return
if any(p.name == name for p in self.profiles): if any(p.name == name for p in self.profiles):
messagebox.showerror( messagebox.showerror("Error", f"A profile with the name '{name}' already exists.", parent=self)
"Error",
f"A profile with the name '{name}' already exists.",
parent=self,
)
return return
new_profile = ExportProfile(name=name.strip()) new_profile = ExportProfile(name=name.strip())
self.profiles.append(new_profile) self.profiles.append(new_profile)
self._load_profiles_to_combobox() self._load_profiles_to_combobox()
@ -549,13 +441,8 @@ class ProfileEditorWindow(tk.Toplevel):
def _on_delete_profile(self): def _on_delete_profile(self):
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile: if not profile: return
return if messagebox.askyesno( "Confirm Delete", f"Are you sure you want to delete the profile '{profile.name}'?", parent=self):
if messagebox.askyesno(
"Confirm Delete",
f"Are you sure you want to delete the profile '{profile.name}'?",
parent=self,
):
self.profiles.remove(profile) self.profiles.remove(profile)
self._load_profiles_to_combobox() self._load_profiles_to_combobox()
@ -570,11 +457,7 @@ class ProfileEditorWindow(tk.Toplevel):
def _on_close(self): def _on_close(self):
if self._check_unsaved_changes(): if self._check_unsaved_changes():
response = messagebox.askyesnocancel( response = messagebox.askyesnocancel("Unsaved Changes", "You have unsaved changes. Would you like to save them?", parent=self)
"Unsaved Changes",
"You have unsaved changes. Would you like to save them?",
parent=self,
)
if response is True: if response is True:
self._on_save_and_close() self._on_save_and_close()
elif response is False: elif response is False: