SXXXXXXX_RadarDataReader/radar_data_reader/gui/main_window.py
2025-06-23 08:00:07 +02:00

490 lines
19 KiB
Python

"""
Main View for the Radar Data Reader application, now featuring a tabbed interface.
"""
import tkinter as tk
from tkinter import scrolledtext, filedialog, ttk
from pathlib import Path
from typing import Dict, Any, List
import queue
from datetime import datetime, timezone
from ..utils import logger
from ..core.export_profiles import ExportProfile
log = logger.get_logger(__name__)
def _format_timetag(timetag: Any) -> str:
"""Safely formats a numeric timetag into a human-readable UTC string."""
if not isinstance(timetag, (int, float)) or timetag <= 0:
return "N/A"
try:
dt_object = datetime.fromtimestamp(timetag, tz=timezone.utc)
return dt_object.strftime("%Y-%m-%d %H:%M:%S UTC")
except (ValueError, OSError):
return f"Invalid timestamp ({timetag})"
class MainWindow(tk.Frame):
"""The main application window (View)."""
def __init__(self, master: tk.Tk, controller, logging_config: Dict[str, Any]):
super().__init__(master)
self.master = master
self.controller = controller
self.gui_update_queue = controller.result_queue
self.total_blocks_for_progress = 0
self.profile_editor_window = None
self._init_vars()
self.pack(fill=tk.BOTH, expand=True)
self.master.title("Radar Data Reader & Processor")
self.master.geometry("800x850")
self._create_widgets()
self._setup_gui_logging(logging_config)
self.master.protocol("WM_DELETE_WINDOW", self.on_close)
log.info("Main window View initialized.")
def _init_vars(self):
"""Initialize all Tkinter variables."""
self.status_bar_var = tk.StringVar(value="Ready")
self.out_filepath_var = tk.StringVar()
self.out_output_dir_var = tk.StringVar()
self.out_basename_var = tk.StringVar()
self.out_output_csv_var = tk.BooleanVar(value=True)
self.out_output_json_var = tk.BooleanVar(value=False)
self.out_csv_profile_var = tk.StringVar()
self.out_json_profile_var = tk.StringVar()
self.rec_filepath_var = tk.StringVar()
self.rec_file_count_var = tk.IntVar(value=1)
self.rec_output_dir_var = tk.StringVar()
self.rec_basename_var = tk.StringVar()
self.rec_output_out_var = tk.BooleanVar(value=True)
self.rec_output_csv_var = tk.BooleanVar(value=False)
self.rec_output_json_var = tk.BooleanVar(value=False)
self.rec_extract_video_var = tk.BooleanVar(value=False)
self.rec_csv_profile_var = tk.StringVar()
self.rec_json_profile_var = tk.StringVar()
self.progress_text_var = tk.StringVar(value="N/A")
self.batches_found_var = tk.StringVar(value="N/A")
self.file_batch_counter_var = tk.StringVar(value="N/A")
self.timetag_var = tk.StringVar(value="N/A")
self.progress_bar_var = tk.DoubleVar(value=0)
def _create_widgets(self):
menu_bar = tk.Menu(self.master)
self.master.config(menu=menu_bar)
file_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label="File", menu=file_menu)
file_menu.add_command(
label="Manage Export Profiles...",
command=self.controller.open_profile_editor,
)
file_menu.add_separator()
file_menu.add_command(label="Exit", command=self.on_close)
main_frame = tk.Frame(self)
main_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
main_frame.columnconfigure(0, weight=1)
notebook = ttk.Notebook(main_frame)
notebook.grid(row=0, column=0, sticky="nsew", pady=(0, 10))
out_processor_tab = ttk.Frame(notebook, padding="10")
rec_processor_tab = ttk.Frame(notebook, padding="10")
notebook.add(out_processor_tab, text="OUT Processor")
notebook.add(rec_processor_tab, text="REC Processor")
self._create_out_processor_tab(out_processor_tab)
self._create_rec_processor_tab(rec_processor_tab)
self._create_live_data_frame(main_frame)
self._create_log_console_frame(main_frame)
self.status_bar = ttk.Label(
self,
textvariable=self.status_bar_var,
relief=tk.SUNKEN,
anchor=tk.W,
padding=2,
)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def _create_out_processor_tab(self, parent):
parent.columnconfigure(1, weight=1)
input_frame = ttk.LabelFrame(parent, text="Input")
input_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
input_frame.columnconfigure(1, weight=1)
ttk.Label(input_frame, text="Input .out File:").grid(
row=0, column=0, padx=5, pady=5, sticky="w"
)
out_file_entry = ttk.Entry(
input_frame, textvariable=self.out_filepath_var, state="readonly"
)
out_file_entry.grid(row=0, column=1, sticky="ew", padx=5)
self.out_browse_button = ttk.Button(
input_frame, text="Browse...", command=self.controller.select_out_file
)
self.out_browse_button.grid(row=0, column=2, padx=5, pady=5)
self.out_filepath_var.trace_add("write", self.controller.on_out_config_changed)
output_frame = ttk.LabelFrame(parent, text="Output Configuration")
output_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
output_frame.columnconfigure(1, weight=1)
ttk.Label(output_frame, text="Output Directory:").grid(
row=0, column=0, padx=5, pady=5, sticky="w"
)
out_dir_entry = ttk.Entry(output_frame, textvariable=self.out_output_dir_var)
out_dir_entry.grid(row=0, column=1, sticky="ew", padx=5)
out_dir_buttons_frame = ttk.Frame(output_frame)
out_dir_buttons_frame.grid(row=0, column=2, padx=5)
ttk.Button(
out_dir_buttons_frame,
text="Browse...",
command=lambda: self.controller.select_output_dir(self.out_output_dir_var),
).pack(side=tk.LEFT)
ttk.Button(
out_dir_buttons_frame,
text="Open...",
command=lambda: self.controller.open_folder_from_path(
self.out_output_dir_var.get()
),
).pack(side=tk.LEFT, padx=(5, 0))
ttk.Label(output_frame, text="Base Filename:").grid(
row=1, column=0, padx=5, pady=5, sticky="w"
)
out_basename_entry = ttk.Entry(output_frame, textvariable=self.out_basename_var)
out_basename_entry.grid(row=1, column=1, columnspan=2, sticky="ew", padx=5)
options_frame = ttk.LabelFrame(parent, text="Output Formats")
options_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
options_frame.columnconfigure(1, weight=1)
ttk.Checkbutton(
options_frame, text="Generate .csv file", variable=self.out_output_csv_var
).grid(row=0, column=0, sticky="w", padx=5, pady=2)
self.out_csv_profile_combobox = ttk.Combobox(
options_frame,
textvariable=self.out_csv_profile_var,
state="readonly",
width=20,
)
self.out_csv_profile_combobox.grid(row=0, column=1, sticky="w", padx=5)
ttk.Checkbutton(
options_frame, text="Generate .json file", variable=self.out_output_json_var
).grid(row=1, column=0, sticky="w", padx=5, pady=2)
self.out_json_profile_combobox = ttk.Combobox(
options_frame,
textvariable=self.out_json_profile_var,
state="readonly",
width=20,
)
self.out_json_profile_combobox.grid(row=1, column=1, sticky="w", padx=5)
action_frame = ttk.Frame(parent)
action_frame.grid(row=3, column=0, columnspan=2, pady=(10, 0))
self.out_process_button = ttk.Button(
action_frame,
text="Process .out File",
command=self.controller.start_out_processing,
)
self.out_process_button.pack(side=tk.LEFT, padx=5)
self.out_stop_button = ttk.Button(
action_frame,
text="Stop",
command=self.controller.stop_processing,
state=tk.DISABLED,
)
self.out_stop_button.pack(side=tk.LEFT, padx=5)
def _create_rec_processor_tab(self, parent):
parent.columnconfigure(1, weight=1)
input_frame = ttk.LabelFrame(parent, text="Input REC Sequence")
input_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
input_frame.columnconfigure(1, weight=1)
ttk.Label(input_frame, text="First .rec File:").grid(
row=0, column=0, padx=5, pady=5, sticky="w"
)
rec_file_entry = ttk.Entry(
input_frame, textvariable=self.rec_filepath_var, state="readonly"
)
rec_file_entry.grid(row=0, column=1, sticky="ew", padx=5)
ttk.Button(
input_frame, text="Browse...", command=self.controller.select_rec_file
).grid(row=0, column=2, padx=5)
ttk.Label(input_frame, text="Number of Files:").grid(
row=1, column=0, padx=5, pady=5, sticky="w"
)
rec_file_count_spinbox = ttk.Spinbox(
input_frame,
from_=1,
to=1000,
textvariable=self.rec_file_count_var,
width=10,
)
rec_file_count_spinbox.grid(row=1, column=1, padx=5, pady=5, sticky="w")
self.rec_file_count_var.trace_add(
"write", self.controller.on_rec_config_changed
)
self.rec_filepath_var.trace_add("write", self.controller.on_rec_config_changed)
output_frame = ttk.LabelFrame(parent, text="Output Configuration")
output_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
output_frame.columnconfigure(1, weight=1)
ttk.Label(output_frame, text="Output Directory:").grid(
row=0, column=0, padx=5, pady=5, sticky="w"
)
rec_dir_entry = ttk.Entry(output_frame, textvariable=self.rec_output_dir_var)
rec_dir_entry.grid(row=0, column=1, sticky="ew", padx=5)
rec_dir_buttons_frame = ttk.Frame(output_frame)
rec_dir_buttons_frame.grid(row=0, column=2, padx=5)
ttk.Button(
rec_dir_buttons_frame,
text="Browse...",
command=lambda: self.controller.select_output_dir(self.rec_output_dir_var),
).pack(side=tk.LEFT)
ttk.Button(
rec_dir_buttons_frame,
text="Open...",
command=lambda: self.controller.open_folder_from_path(
self.rec_output_dir_var.get()
),
).pack(side=tk.LEFT, padx=(5, 0))
ttk.Label(output_frame, text="Base Filename:").grid(
row=1, column=0, padx=5, pady=5, sticky="w"
)
rec_basename_entry = ttk.Entry(output_frame, textvariable=self.rec_basename_var)
rec_basename_entry.grid(row=1, column=1, columnspan=2, sticky="ew", padx=5)
options_frame = ttk.LabelFrame(parent, text="Output Formats")
options_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
options_frame.columnconfigure(1, weight=1)
ttk.Checkbutton(
options_frame, text="Generate .out file", variable=self.rec_output_out_var
).grid(row=0, column=0, columnspan=2, sticky="w", padx=5)
ttk.Checkbutton(
options_frame,
text="Extract video stream",
variable=self.rec_extract_video_var,
).grid(row=1, column=0, columnspan=2, sticky="w", padx=5)
ttk.Checkbutton(
options_frame, text="Generate .csv file", variable=self.rec_output_csv_var
).grid(row=2, column=0, sticky="w", padx=5)
self.rec_csv_profile_combobox = ttk.Combobox(
options_frame,
textvariable=self.rec_csv_profile_var,
state="readonly",
width=20,
)
self.rec_csv_profile_combobox.grid(row=2, column=1, sticky="w", padx=5)
ttk.Checkbutton(
options_frame, text="Generate .json file", variable=self.rec_output_json_var
).grid(row=3, column=0, sticky="w", padx=5)
self.rec_json_profile_combobox = ttk.Combobox(
options_frame,
textvariable=self.rec_json_profile_var,
state="readonly",
width=20,
)
self.rec_json_profile_combobox.grid(row=3, column=1, sticky="w", padx=5)
action_frame = ttk.Frame(parent)
action_frame.grid(row=3, column=0, columnspan=2, pady=(10, 0))
self.rec_process_button = ttk.Button(
action_frame,
text="Process REC Sequence",
command=self.controller.start_rec_processing,
)
self.rec_process_button.pack()
def _create_live_data_frame(self, parent):
status_frame = ttk.LabelFrame(parent, text="Live Data & Progress")
status_frame.grid(row=1, column=0, sticky="nsew", pady=(0, 5))
status_frame.columnconfigure(1, weight=1)
self.progress_bar = ttk.Progressbar(
status_frame, variable=self.progress_bar_var, maximum=100
)
self.progress_bar.grid(
row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(5, 2)
)
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="Batches Found:", anchor="e").grid(
row=2, column=0, padx=(10, 5), pady=2, sticky="e"
)
ttk.Label(status_frame, textvariable=self.batches_found_var, anchor="w").grid(
row=2, column=1, padx=5, pady=2, sticky="w"
)
ttk.Label(status_frame, text="File Batch Counter:", anchor="e").grid(
row=3, column=0, padx=(10, 5), pady=2, sticky="e"
)
ttk.Label(
status_frame, textvariable=self.file_batch_counter_var, anchor="w"
).grid(row=3, column=1, padx=5, pady=2, sticky="w")
ttk.Label(status_frame, text="File TimeTag:", anchor="e").grid(
row=4, column=0, padx=(10, 5), pady=(2, 5), sticky="e"
)
ttk.Label(status_frame, textvariable=self.timetag_var, anchor="w").grid(
row=4, column=1, padx=5, pady=(2, 5), sticky="w"
)
def _create_log_console_frame(self, parent):
log_frame = ttk.LabelFrame(parent, text="Log Console")
log_frame.grid(row=2, column=0, sticky="nsew", pady=(5, 0))
log_frame.rowconfigure(0, weight=1)
log_frame.columnconfigure(0, weight=1)
self.log_widget = scrolledtext.ScrolledText(
log_frame, state=tk.DISABLED, wrap=tk.WORD
)
self.log_widget.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
parent.rowconfigure(2, weight=1)
def _setup_gui_logging(self, logging_config):
logger.add_tkinter_handler(self.log_widget, self.master, logging_config)
def update_export_profiles(
self,
profiles: List[ExportProfile],
active_out_profile: str,
active_rec_csv: str,
active_rec_json: str,
):
profile_names = [p.name for p in profiles] if profiles else []
all_combos = [
(
self.out_csv_profile_combobox,
self.out_csv_profile_var,
active_out_profile,
),
(
self.out_json_profile_combobox,
self.out_json_profile_var,
active_out_profile,
),
(self.rec_csv_profile_combobox, self.rec_csv_profile_var, active_rec_csv),
(
self.rec_json_profile_combobox,
self.rec_json_profile_var,
active_rec_json,
),
]
for combo, var, active_name in all_combos:
combo["values"] = profile_names
if active_name in profile_names:
var.set(active_name)
elif profile_names:
var.set(profile_names[0])
else:
var.set("")
def start_processing_ui(self):
self.update_ui_for_processing_state(True)
self.progress_text_var.set("Starting...")
self.batches_found_var.set("N/A")
self.file_batch_counter_var.set("N/A")
self.timetag_var.set("N/A")
self.progress_bar_var.set(0)
self.after(100, self.poll_result_queue)
def update_ui_for_processing_state(self, is_processing: bool):
state = tk.DISABLED if is_processing else tk.NORMAL
# Disable all major controls on both tabs
for widget in [
self.out_browse_button,
self.out_process_button,
self.rec_process_button,
]:
if widget.winfo_exists():
widget.config(state=state)
# Specifically manage stop buttons, assuming one shared stop button for now
self.out_stop_button.config(state=tk.NORMAL if is_processing else tk.DISABLED)
if is_processing:
self.status_bar_var.set("Processing... Please wait.")
self.master.config(cursor="watch")
else:
self.status_bar_var.set("Ready")
self.progress_bar_var.set(0)
self.progress_text_var.set("Done")
self.master.config(cursor="")
def poll_result_queue(self):
try:
while True:
msg = self.gui_update_queue.get_nowait()
msg_type = msg.get("type")
if msg_type == "start":
self.total_blocks_for_progress = msg.get("total", 0)
elif msg_type == "progress":
batch_id = msg.get("batch_id", "N/A")
file_batch_counter = msg.get("file_batch_counter", "N/A")
timetag = msg.get("timetag", "N/A")
blocks_done = msg.get("blocks_done", 0)
self.batches_found_var.set(str(batch_id))
self.progress_text_var.set(
f"Block {blocks_done} / {self.total_blocks_for_progress}"
)
self.file_batch_counter_var.set(str(file_batch_counter))
self.timetag_var.set(f"{timetag} ({_format_timetag(timetag)})")
if self.total_blocks_for_progress > 0:
progress = (blocks_done / self.total_blocks_for_progress) * 100
self.progress_bar_var.set(progress)
elif msg_type == "data_batch":
self.controller.handle_data_batch(msg.get("data"))
elif msg_type == "complete":
self.controller.handle_worker_completion(msg)
self.update_ui_for_processing_state(False)
return
elif msg_type == "error":
log.error(f"Received error from worker: {msg.get('message')}")
self.controller.handle_worker_completion(msg)
self.update_ui_for_processing_state(False)
self.progress_text_var.set("Error!")
return
except queue.Empty:
pass
except Exception as e:
log.error(f"Error in GUI polling loop: {e}")
if self.controller.is_processing:
self.after(100, self.poll_result_queue)
def on_stop_click(self):
self.controller.stop_processing()
def on_close(self):
self.controller.shutdown()
self.master.destroy()