add report info into log, change gui

This commit is contained in:
VALLONGOL 2025-06-20 12:23:22 +02:00
parent f9383140fb
commit 9161e1f6b5
6 changed files with 202 additions and 80 deletions

View File

@ -8,27 +8,38 @@
"fields": [
{
"column_name": "batch_id",
"data_path": "batch_id"
"data_path": "batch_id",
"translate_with_enum": false
},
{
"column_name": "batch_counter",
"data_path": "main_header.ge_header.signal_descr.batch_counter"
"data_path": "main_header.ge_header.signal_descr.batch_counter",
"translate_with_enum": false
},
{
"column_name": "ttag",
"data_path": "main_header.ge_header.signal_descr.ttag"
"data_path": "main_header.ge_header.signal_descr.ttag",
"translate_with_enum": false
},
{
"column_name": "master_mode",
"data_path": "main_header.ge_header.mode.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"
"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"
"data_path": "main_header.ge_header.mode.range_scale",
"translate_with_enum": false
},
{
"column_name": "waveform",
"data_path": "main_header.ge_header.mode.waveform",
"translate_with_enum": true
}
]
}

View File

@ -15,17 +15,21 @@ from typing import List, Any, Dict
from ..utils.config_manager import ConfigManager
from ..core.file_reader import run_worker_process
from ..core.data_structures import DataBatch
from ..core.data_enums import ENUM_REGISTRY, get_enum_name
from ..utils import logger
from ..gui.profile_editor_window import ProfileEditorWindow
from ..core.export_profiles import ExportProfile
from ..core.export_profiles import ExportProfile, ExportField
log = logger.get_logger(__name__)
def _get_value_from_path(batch: DataBatch, path: str) -> Any:
def _get_value_from_path(batch: DataBatch, field: ExportField) -> Any:
"""
Safely retrieves a value from a DataBatch object by navigating a dot-separated path.
Safely retrieves a value from a DataBatch object using the path from an ExportField.
If the field is marked for translation, it attempts to convert the numeric value
to its string representation using the corresponding Enum.
"""
path = field.data_path
try:
parts = path.split('.')
if not parts:
@ -40,7 +44,15 @@ def _get_value_from_path(batch: DataBatch, path: str) -> Any:
return "N/A"
current_obj = getattr(current_obj, part, None)
return current_obj if current_obj is not None else "N/A"
value = current_obj if current_obj is not None else "N/A"
# --- ENUM TRANSLATION LOGIC ---
if field.translate_with_enum and isinstance(value, int):
enum_class = ENUM_REGISTRY.get(path)
if enum_class:
return get_enum_name(enum_class, value)
return value
except AttributeError:
log.warning(f"Could not find attribute for path: {path}")
@ -83,15 +95,14 @@ class AppController:
self.view.update_export_profiles(profiles, active_profile_name)
def _propose_output_filepath(self, input_path_str: str):
"""Proposes a default output path based on the input path."""
if not input_path_str:
return
proposed_path = Path(input_path_str).with_suffix('.csv')
self.view.set_output_filepath(str(proposed_path))
def select_file(self):
self.current_path = self.view.get_filepath()
if filepath := self.view.ask_open_filename(self.current_path):
current_path = self.view.get_filepath()
if filepath := self.view.ask_open_filename(current_path):
self.view.set_filepath(filepath)
self._propose_output_filepath(filepath)
@ -190,14 +201,14 @@ class AppController:
return
try:
row_values = [_get_value_from_path(batch, field.data_path) for field in self.active_export_profile.fields]
# Pass the entire ExportField object to the helper function
row_values = [_get_value_from_path(batch, field) for field in self.active_export_profile.fields]
self.csv_writer.writerow(row_values)
self.csv_file_handle.flush()
except Exception as e:
log.error(f"An unexpected error occurred during CSV row writing: {e}", exc_info=True)
def _log_summary(self, stats: Dict[str, int]):
"""Formats and logs a summary of the processing results."""
log.info("--- Processing Summary ---")
batches = stats.get("batches_processed", 0)
@ -245,7 +256,6 @@ class AppController:
self.is_processing = False
self.worker_process = None
# Log the final summary
if stats := msg.get("stats"):
self._log_summary(stats)

View File

@ -0,0 +1,69 @@
"""
This module contains Enum definitions that mirror the C++ enums,
used for translating numeric data fields into human-readable strings.
"""
from enum import Enum, unique
# Using @unique ensures that no two enum members have the same value.
# We use a helper function to handle unknown values gracefully.
def get_enum_name(enum_class, value):
"""Tries to get the name of an enum member, returns the value itself if not found."""
try:
return enum_class(value).name
except ValueError:
return value
@unique
class MasterMode(Enum):
"""Maps to the m2str function in postprocess.cpp"""
IDLE = 0
IBIT = 1
GM = 2
DBS = 3
RWS = 4
VS = 5
ACM = 6
TWS = 7
SEA1 = 8
SEA2 = 9
GMTI = 10
BCN = 11
SAM = 12
TA = 13
WA = 14
STT = 15
DTT = 16
SSTT = 17
ACQ = 18
FTT = 19
AGR = 20
SAR = 21
INV = 22
DUMMY = 23
ETET = 24
BOOT = 25
@unique
class Waveform(Enum):
"""Maps to the wf2str function in postprocess.cpp"""
IDLE = 0
LPRF = 1
MPRF = 2
HPRF = 3
LPRFNC = 4
MPRF_SHORT = 5
AGR_SHORT = 6
AGR_LONG = 7
MPRF_LOOK_UP = 8
MPRF_ADAPTIVE = 9
AUTO_WITHOUT_LPRF_NC = 10
# This dictionary acts as a registry, mapping a data path to the Enum class
# that can be used to translate its value. This will be used by the Profile Editor.
ENUM_REGISTRY = {
"main_header.ge_header.mode.master_mode": MasterMode,
"main_header.ge_header.mode.waveform": Waveform,
# Add other mappings here as we identify them
# "path.to.another.field": AnotherEnum,
}

View File

@ -2,7 +2,7 @@
Defines the data structures for managing export profiles.
"""
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional
from typing import List, Dict, Any
# Using slots for performance, similar to data_structures.py
DC_KWARGS = {'slots': True}
@ -16,21 +16,20 @@ class ExportField:
Attributes:
column_name (str): The name of the column in the CSV file header.
data_path (str): A dot-separated path to access the value within
the DataBatch object. E.g., "header.header_data.signal_descr.ttag".
the DataBatch object. E.g., "main_header.ge_header.mode.master_mode".
translate_with_enum (bool): If True, the numeric value will be translated
to its string representation using a corresponding Enum.
Defaults to False.
"""
column_name: str
data_path: str
translate_with_enum: bool = False
@dataclass(**DC_KWARGS)
class ExportProfile:
"""
Represents a full export profile, including its name and the list of fields.
Attributes:
name (str): The unique name of the profile (e.g., "Navigation Data").
fields (List[ExportField]): An ordered list of fields to export. The order
determines the column order in the CSV.
"""
name: str
fields: List[ExportField] = field(default_factory=list)
@ -40,7 +39,11 @@ class ExportProfile:
return {
"name": self.name,
"fields": [
{"column_name": f.column_name, "data_path": f.data_path}
{
"column_name": f.column_name,
"data_path": f.data_path,
"translate_with_enum": f.translate_with_enum,
}
for f in self.fields
],
}
@ -50,5 +53,16 @@ class ExportProfile:
"""Creates an ExportProfile instance from a dictionary."""
name = data.get("name", "Unnamed Profile")
fields_data = data.get("fields", [])
fields = [ExportField(**field_data) for field_data in fields_data]
fields = []
for field_data in fields_data:
# Ensure backward compatibility with old configs that don't have the flag
translate_flag = field_data.get("translate_with_enum", False)
fields.append(
ExportField(
column_name=field_data.get("column_name", ""),
data_path=field_data.get("data_path", ""),
translate_with_enum=translate_flag,
)
)
return ExportProfile(name=name, fields=fields)

View File

@ -124,10 +124,10 @@ class MainWindow(tk.Frame):
ttk.Label(status_frame, text="Progress:", anchor="e").grid(row=1, column=0, padx=(10, 5), pady=2, sticky="e")
ttk.Label(status_frame, textvariable=self.progress_text_var, anchor="w").grid(row=1, column=1, padx=5, pady=2, sticky="w")
ttk.Label(status_frame, text="File Batch Counter:", anchor="e").grid(row=2, column=0, padx=(10, 5), pady=2, sticky="e")
ttk.Label(status_frame, text="Batch Counter:", anchor="e").grid(row=2, column=0, padx=(10, 5), pady=2, sticky="e")
ttk.Label(status_frame, textvariable=self.file_batch_counter_var, anchor="w").grid(row=2, column=1, padx=5, pady=2, sticky="w")
ttk.Label(status_frame, text="File TimeTag:", anchor="e").grid(row=3, column=0, padx=(10, 5), pady=(2, 5), sticky="e")
ttk.Label(status_frame, text="TimeTag:", anchor="e").grid(row=3, column=0, padx=(10, 5), pady=(2, 5), sticky="e")
ttk.Label(status_frame, textvariable=self.timetag_var, anchor="w").grid(row=3, column=1, padx=5, pady=(2, 5), sticky="w")
# --- Log Console Frame ---

View File

@ -9,6 +9,7 @@ import copy
from typing import List, Type, Dict, Any, Union, Optional
from ..core import data_structures as ds
from ..core.data_enums import ENUM_REGISTRY
from ..core.export_profiles import ExportProfile, ExportField
from ..utils import logger
@ -40,20 +41,20 @@ class ProfileEditorWindow(tk.Toplevel):
def _init_window(self):
"""Initializes window properties."""
self.title("Export Profile Editor")
self.geometry("900x600")
self.geometry("1000x600") # Increased width for new column
self.transient(self.master)
self.grab_set()
def _init_vars(self):
"""Initializes Tkinter variables."""
self.selected_profile_name = tk.StringVar()
self.selected_fields_var = tk.Variable(value=[])
def _create_widgets(self):
"""Creates the main layout and widgets for the editor."""
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# --- Left Frame: Profile Management ---
profile_mgmt_frame = ttk.LabelFrame(main_pane, text="Profiles")
main_pane.add(profile_mgmt_frame, weight=1)
profile_mgmt_frame.columnconfigure(0, weight=1)
@ -67,10 +68,11 @@ class ProfileEditorWindow(tk.Toplevel):
btn_frame = ttk.Frame(profile_mgmt_frame)
btn_frame.grid(row=1, column=0, sticky="ew", padx=5)
btn_frame.columnconfigure((0, 1, 2), weight=1)
btn_frame.columnconfigure((0, 1), weight=1)
ttk.Button(btn_frame, text="New", command=self._on_new_profile).grid(row=0, column=0, sticky="ew", padx=2)
ttk.Button(btn_frame, text="Delete", command=self._on_delete_profile).grid(row=0, column=1, sticky="ew", padx=2)
# --- Middle Frame: Available Fields ---
fields_frame = ttk.LabelFrame(main_pane, text="Available Fields")
main_pane.add(fields_frame, weight=2)
fields_frame.rowconfigure(0, weight=1)
@ -81,8 +83,9 @@ class ProfileEditorWindow(tk.Toplevel):
self.fields_tree.configure(yscrollcommand=ysb.set)
ysb.grid(row=0, column=1, sticky='ns')
# --- Right Frame: Selected Fields and Actions ---
selected_frame_container = ttk.Frame(main_pane)
main_pane.add(selected_frame_container, weight=2)
main_pane.add(selected_frame_container, weight=3)
selected_frame_container.rowconfigure(0, weight=1)
selected_frame_container.columnconfigure(1, weight=1)
@ -97,34 +100,53 @@ class ProfileEditorWindow(tk.Toplevel):
selected_fields_frame.grid(row=0, column=1, sticky="nsew")
selected_fields_frame.rowconfigure(0, weight=1)
selected_fields_frame.columnconfigure(0, weight=1)
self.selected_listbox = tk.Listbox(selected_fields_frame, listvariable=self.selected_fields_var, selectmode=tk.SINGLE)
self.selected_listbox.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
ysb_list = ttk.Scrollbar(selected_fields_frame, orient='vertical', command=self.selected_listbox.yview)
self.selected_listbox.config(yscrollcommand=ysb_list.set)
ysb_list.grid(row=0, column=1, sticky='ns')
self.selected_tree = ttk.Treeview(selected_fields_frame, columns=("display_name", "translate"), show="headings", selectmode="browse")
self.selected_tree.heading("display_name", text="Field Name")
self.selected_tree.heading("translate", text="Translate")
self.selected_tree.column("display_name", width=200, stretch=True)
self.selected_tree.column("translate", width=100, anchor="center", stretch=False)
self.selected_tree.grid(row=0, column=0, sticky="nsew")
self.selected_tree.bind("<Button-1>", self._on_selected_tree_click)
# --- Bottom Frame: Save/Cancel ---
bottom_frame = ttk.Frame(self)
bottom_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
ttk.Button(bottom_frame, text="Save & Close", command=self._on_save_and_close).pack(side=tk.RIGHT)
ttk.Button(bottom_frame, text="Cancel", command=self._on_close).pack(side=tk.RIGHT, padx=5)
def _on_selected_tree_click(self, event):
region = self.selected_tree.identify_region(event.x, event.y)
if region != "cell":
return
column_id = self.selected_tree.identify_column(event.x)
if column_id != "#2": # Column index for "translate"
return
item_id = self.selected_tree.identify_row(event.y)
if not item_id:
return
profile = self._get_current_profile()
if not profile:
return
field_index = int(item_id)
field = profile.fields[field_index]
if field.data_path in ENUM_REGISTRY:
field.translate_with_enum = not field.translate_with_enum
self._load_profile_into_ui()
def _populate_available_fields_tree(self):
"""Introspects the data structures and populates the Treeview correctly."""
self.fields_tree.delete(*self.fields_tree.get_children())
# --- Root 1: Batch-level properties ---
batch_root = self.fields_tree.insert("", "end", iid="batch_properties", text="Batch Properties")
# Add batch_id directly as it's a top-level property
self.fields_tree.insert(batch_root, "end", iid="batch_id", text="batch_id", values=("batch_id", "batch_id"))
# --- Root 2: Header data properties ---
# The user is interested in the contents of the header block.
header_root = self.fields_tree.insert("", "end", iid="header_data", text="Header Data (from DspHeaderIn)")
# We start the recursion from GeHeader, with a base path pointing to it.
header_root = self.fields_tree.insert("", "end", iid="header_data", text="Header Data")
self._recursive_populate_tree(ds.GeHeader, header_root, "main_header.ge_header")
def _recursive_populate_tree(self, class_obj: Type, parent_id: str, base_path: str):
"""Recursively explores dataclasses to build the tree, starting from a base path."""
if not dataclasses.is_dataclass(class_obj):
return
@ -133,7 +155,6 @@ class ProfileEditorWindow(tk.Toplevel):
current_path = f"{base_path}.{field_name}"
node_id = f"{parent_id}_{field_name}"
# Determine the type to inspect, handling Optional and List
type_to_inspect = field.type
if hasattr(type_to_inspect, '__origin__') and type_to_inspect.__origin__ in [Union, list]:
args = getattr(type_to_inspect, '__args__', [])
@ -141,15 +162,13 @@ class ProfileEditorWindow(tk.Toplevel):
if non_none_args:
type_to_inspect = non_none_args[0]
# If it's another dataclass, create a branch and recurse
if dataclasses.is_dataclass(type_to_inspect):
child_node = self.fields_tree.insert(parent_id, "end", iid=node_id, text=field_name)
self._recursive_populate_tree(type_to_inspect, node_id, current_path)
# If it's a simple type, create a leaf
else:
display_text = f"{field_name}"
if field.type in LEAF_TYPES:
display_text = f"{field_name} [{field.type.__name__}]"
if base_path + "." + field_name in ENUM_REGISTRY:
display_text += " (Enum)"
self.fields_tree.insert(parent_id, "end", iid=node_id, text=display_text, values=(field_name, current_path))
def _load_profiles_to_combobox(self):
@ -157,7 +176,7 @@ class ProfileEditorWindow(tk.Toplevel):
self.profile_combobox['values'] = profile_names
if profile_names:
self.selected_profile_name.set(profile_names[0])
self._load_profile_into_ui()
self._load_profile_into_ui()
def _get_current_profile(self) -> Optional[ExportProfile]:
name = self.selected_profile_name.get()
@ -167,41 +186,49 @@ class ProfileEditorWindow(tk.Toplevel):
self._load_profile_into_ui()
def _load_profile_into_ui(self):
for i in self.selected_tree.get_children():
self.selected_tree.delete(i)
profile = self._get_current_profile()
if not profile:
self.selected_fields_var.set([])
return
display_list = [f.column_name for f in profile.fields]
self.selected_fields_var.set(display_list)
for index, field in enumerate(profile.fields):
is_translatable = field.data_path in ENUM_REGISTRY
checkbox_char = ""
if is_translatable:
checkbox_char = "" if field.translate_with_enum else ""
self.selected_tree.insert(
"", "end", iid=str(index),
values=(field.column_name, checkbox_char)
)
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 (a leaf node), not a category.", parent=self)
messagebox.showinfo("Cannot Add Field", "Please select a specific data field.", 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", f"The field '{column_name}' is already in the profile.", parent=self)
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))
self._load_profile_into_ui()
def _remove_field(self):
selection = self.selected_listbox.curselection()
selection = self.selected_tree.selection()
if not selection: return
index_to_remove = selection[0]
index_to_remove = int(selection[0])
profile = self._get_current_profile()
if not profile: return
@ -209,10 +236,10 @@ class ProfileEditorWindow(tk.Toplevel):
self._load_profile_into_ui()
def _move_field(self, direction: int):
selection = self.selected_listbox.curselection()
selection = self.selected_tree.selection()
if not selection: return
index = selection[0]
index = int(selection[0])
new_index = index + direction
profile = self._get_current_profile()
@ -222,12 +249,11 @@ class ProfileEditorWindow(tk.Toplevel):
fields = profile.fields
fields.insert(new_index, fields.pop(index))
self._load_profile_into_ui()
self.selected_listbox.selection_set(new_index)
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
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)
@ -258,16 +284,8 @@ 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 before closing?",
parent=self
)
if response is True:
self._on_save_and_close()
elif response is False:
self.destroy()
else:
return
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()