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",
"data_path": "main_header.ge_header.mode.scan_rate",
"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",
"data_path": "d1553_data.ground_track_angle_deg",
"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,
# AESA Block -> Synthetic Report
"aesa_data.payload.comm": AesaError,
# D1553
"d1553_data.raw_mode": MasterMode,
}

View File

@ -231,6 +231,29 @@ class D1553Block(BaseBlock):
self._ensure_cache()
return self._cache.range_scale_nm
@property
def raw_range_scale(self) -> Optional[int]:
self._ensure_cache()
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()

View File

@ -5,7 +5,7 @@ from tkinter import ttk, simpledialog, messagebox
import ctypes
import copy
import re
import inspect # Import necessary for inspecting properties
import inspect
from typing import List, Type, Dict, Any, Optional
from .gui_utils import center_window
@ -76,6 +76,7 @@ class ProfileEditorWindow(tk.Toplevel):
self._init_window()
self._init_vars()
self._create_widgets()
# Initial population of the tree
self._populate_available_fields_tree()
self._load_profiles_to_combobox()
center_window(self, self.master)
@ -118,6 +119,8 @@ class ProfileEditorWindow(tk.Toplevel):
fields_frame.columnconfigure(0, weight=1)
self.fields_tree = ttk.Treeview(fields_frame, selectmode="browse")
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(
fields_frame, orient="vertical", command=self.fields_tree.yview
)
@ -155,6 +158,10 @@ class ProfileEditorWindow(tk.Toplevel):
show="headings",
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("data_path", text="Source Path")
self.selected_tree.heading("translate", text="Translate")
@ -192,7 +199,7 @@ class ProfileEditorWindow(tk.Toplevel):
dialog = EditPathDialog(self, initial_value=field.data_path)
new_path = dialog.wait_for_input()
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(
"Invalid Path", "The path contains invalid characters.", parent=self
)
@ -205,7 +212,7 @@ class ProfileEditorWindow(tk.Toplevel):
if region != "cell":
return
column_id = self.selected_tree.identify_column(event.x)
if column_id != "#3":
if column_id != "#3": # Only act on the "Translate" column
return
item_id = self.selected_tree.identify_row(event.y)
if not item_id:
@ -218,26 +225,33 @@ class ProfileEditorWindow(tk.Toplevel):
base_path = re.sub(r"\[\d+\]", "", field.data_path)
if base_path in ENUM_REGISTRY:
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):
# 1. Update the "Selected Fields" tree
for i in self.selected_tree.get_children():
self.selected_tree.delete(i)
profile = self._get_current_profile()
if not profile:
self._update_available_fields_tree_tags() # Still update tags for empty profile
return
for index, field in enumerate(profile.fields):
base_path = re.sub(r"\[\d+\]", "", field.data_path)
is_translatable = base_path in ENUM_REGISTRY
checkbox_char = (
"" if is_translatable and field.translate_with_enum else ""
)
checkbox_char = "" if field.translate_with_enum else ""
tag = "translatable" if is_translatable else "not_translatable"
self.selected_tree.insert(
"",
"end",
iid=str(index),
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):
profile = self._get_current_profile()
@ -253,184 +267,58 @@ class ProfileEditorWindow(tk.Toplevel):
def _populate_available_fields_tree(self):
self.fields_tree.delete(*self.fields_tree.get_children())
# --- Helper to create a non-selectable root node ---
def add_virtual_root(iid, text):
return self.fields_tree.insert("", "end", iid=iid, text=text, open=False)
# --- Batch Properties (standalone fields) ---
batch_root = self.fields_tree.insert(
"", "end", iid="batch_properties", text="Batch Properties"
)
self.fields_tree.insert(
batch_root,
"end",
iid="batch_id",
text="batch_id",
values=("batch_id", "batch_id"),
)
# --- Batch Properties ---
batch_root = add_virtual_root("batch_properties", "Batch Properties")
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"])
# --- DSPHDRIN Data ---
header_root = self.fields_tree.insert(
"", "end", iid="header_data", text="Header Data (from DSPHDRIN)"
)
self._recursive_populate_tree_ctypes(
ds.GeHeader, header_root, "main_header.ge_header"
)
header_root = add_virtual_root("header_data", "Header Data (from DSPHDRIN)")
self._recursive_populate_tree_ctypes(ds.GeHeader, header_root, "main_header.ge_header")
# --- CDPSTS Data ---
cdpsts_root = self.fields_tree.insert(
"", "end", iid="cdpsts_data", text="CDP/STS Block Data"
)
self._recursive_populate_tree_ctypes(
ds.CdpDataLayout, cdpsts_root, "cdp_sts_results.payload.data"
)
cdpsts_root = add_virtual_root("cdpsts_data", "CDP/STS Block Data")
self._recursive_populate_tree_ctypes(ds.CdpStsPayload, cdpsts_root, "cdp_sts_results.payload")
# --- D1553 Data (with visual grouping) ---
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_root = self.fields_tree.insert(
"", "end", iid="timer_data", text="Timer Block Data"
)
self._recursive_populate_tree_ctypes(
ds.GrifoTimerBlob, timer_root, "timer_data.blob"
)
timer_root = add_virtual_root("timer_data", "Timer Block Data")
self._recursive_populate_tree_ctypes(ds.GrifoTimerBlob, timer_root, "timer_data.blob")
# --- AESA Data ---
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"
)
# ... (add other blocks like AESA, EXP, etc. as needed, following this pattern) ...
def _update_available_fields_tree_tags(self):
profile = self._get_current_profile()
used_paths = {f.data_path for f in profile.fields} if profile else set()
# --- D1553 Data ---
d1553_root = self.fields_tree.insert(
"", "end", iid="d1553_data", text="D1553 Block Data"
)
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"
)
for item_id in self._get_all_tree_children(self.fields_tree, ""):
item_values = self.fields_tree.item(item_id, "values")
if item_values and len(item_values) >= 2:
data_path = item_values[1]
if data_path in used_paths:
self.fields_tree.item(item_id, tags=("used_field",))
else:
self.fields_tree.item(item_id, tags=())
# --- EXPANDER Data ---
exp_root = self.fields_tree.insert(
"", "end", iid="exp_data", text="Expander Block Data"
)
# Add the raw_data_bytes field for Expander
self.fields_tree.insert(
exp_root,
"end",
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 ---
pc_root = self.fields_tree.insert(
"", "end", iid="pc_data", text="PC Block Data"
)
# Add the raw_data_bytes field for PC
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 _get_all_tree_children(self, tree, item):
children = tree.get_children(item)
for child in children:
yield child
yield from self._get_all_tree_children(tree, child)
def _recursive_populate_tree_ctypes(
self, class_obj: Type[ctypes.Structure], parent_id: str, base_path: str
@ -440,38 +328,51 @@ class ProfileEditorWindow(tk.Toplevel):
for field_name, field_type in class_obj._fields_:
current_path = f"{base_path}.{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(
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(
field_type, child_node, current_path
)
elif hasattr(field_type, "_length_"):
elif hasattr(field_type, "_length_"): # It's an array
self.fields_tree.insert(
parent_id,
"end",
iid=node_id,
text=f"{field_name} [Array]",
values=(field_name, current_path),
parent_id, "end", 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}"
if current_path in ENUM_REGISTRY:
display_text += " (Enum)"
self.fields_tree.insert(
parent_id,
"end",
iid=node_id,
text=display_text,
values=(field_name, current_path),
parent_id, "end", 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):
profile_names = [p.name for p in self.profiles]
self.profile_combobox["values"] = profile_names
if profile_names:
self.selected_profile_name.set(profile_names[0])
else:
self.selected_profile_name.set("")
self._load_profile_into_ui()
def _get_current_profile(self) -> Optional[ExportProfile]:
@ -483,64 +384,55 @@ class ProfileEditorWindow(tk.Toplevel):
def _add_field(self):
selected_item_id = self.fields_tree.focus()
if not selected_item_id:
return
if not selected_item_id: return
item_values = self.fields_tree.item(selected_item_id, "values")
if not item_values or len(item_values) < 2:
messagebox.showinfo(
"Cannot Add Field", "Please select a specific data field.", parent=self
)
messagebox.showinfo( "Cannot Add Field", "Please select a specific data field, not a category.", parent=self)
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
profile = self._get_current_profile()
if not profile:
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
if not profile: return
profile.fields.append(ExportField(column_name=column_name, data_path=data_path))
self._load_profile_into_ui()
def _remove_field(self):
selection = self.selected_tree.selection()
if not selection:
return
if not selection: return
index_to_remove = int(selection[0])
profile = self._get_current_profile()
if not profile:
return
if not profile: return
del profile.fields[index_to_remove]
self._load_profile_into_ui()
def _move_field(self, direction: int):
selection = self.selected_tree.selection()
if not selection:
return
if not selection: return
index = int(selection[0])
new_index = index + direction
profile = self._get_current_profile()
if not profile or not (0 <= new_index < len(profile.fields)):
return
if not profile or not (0 <= new_index < len(profile.fields)): return
fields = profile.fields
fields.insert(new_index, fields.pop(index))
self._load_profile_into_ui()
self.selected_tree.selection_set(str(new_index))
def _on_new_profile(self):
name = simpledialog.askstring(
"New Profile", "Enter a name for the new profile:", parent=self
)
if not name or not name.strip():
return
name = simpledialog.askstring( "New Profile", "Enter a name for the new profile:", parent=self)
if not name or not name.strip(): return
if any(p.name == name for p in self.profiles):
messagebox.showerror(
"Error",
f"A profile with the name '{name}' already exists.",
parent=self,
)
messagebox.showerror("Error", f"A profile with the name '{name}' already exists.", parent=self)
return
new_profile = ExportProfile(name=name.strip())
self.profiles.append(new_profile)
self._load_profiles_to_combobox()
@ -549,13 +441,8 @@ class ProfileEditorWindow(tk.Toplevel):
def _on_delete_profile(self):
profile = self._get_current_profile()
if not profile:
return
if messagebox.askyesno(
"Confirm Delete",
f"Are you sure you want to delete the profile '{profile.name}'?",
parent=self,
):
if not profile: return
if messagebox.askyesno( "Confirm Delete", f"Are you sure you want to delete the profile '{profile.name}'?", parent=self):
self.profiles.remove(profile)
self._load_profiles_to_combobox()
@ -570,14 +457,10 @@ class ProfileEditorWindow(tk.Toplevel):
def _on_close(self):
if self._check_unsaved_changes():
response = messagebox.askyesnocancel(
"Unsaved Changes",
"You have unsaved changes. Would you like to save them?",
parent=self,
)
response = messagebox.askyesnocancel("Unsaved Changes", "You have unsaved changes. Would you like to save them?", parent=self)
if response is True:
self._on_save_and_close()
elif response is False:
self.destroy()
else:
self.destroy()
self.destroy()