From db0e26b5a1c6911361225a3ede8b13b060eb0d94 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Wed, 25 Jun 2025 15:22:14 +0200 Subject: [PATCH] fix d1553 struct --- config/config.json | 20 + radar_data_reader/core/data_enums.py | 2 + .../core/structures/d1553_structures.py | 23 ++ .../gui/profile_editor_window.py | 349 ++++++------------ 4 files changed, 161 insertions(+), 233 deletions(-) diff --git a/config/config.json b/config/config.json index 0873bb5..4059e03 100644 --- a/config/config.json +++ b/config/config.json @@ -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 } ] } diff --git a/radar_data_reader/core/data_enums.py b/radar_data_reader/core/data_enums.py index 57b0c07..13ccd9b 100644 --- a/radar_data_reader/core/data_enums.py +++ b/radar_data_reader/core/data_enums.py @@ -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, } diff --git a/radar_data_reader/core/structures/d1553_structures.py b/radar_data_reader/core/structures/d1553_structures.py index 7f219aa..dff6737 100644 --- a/radar_data_reader/core/structures/d1553_structures.py +++ b/radar_data_reader/core/structures/d1553_structures.py @@ -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() diff --git a/radar_data_reader/gui/profile_editor_window.py b/radar_data_reader/gui/profile_editor_window.py index 67f503c..06990c8 100644 --- a/radar_data_reader/gui/profile_editor_window.py +++ b/radar_data_reader/gui/profile_editor_window.py @@ -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() \ No newline at end of file